From d09034e0654db4133b4110ea1a2c06f3074a9cef Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Wed, 23 Sep 2015 09:08:53 -0400 Subject: [PATCH] Improve team testing of routers --- .../common/js/spec_helpers/page_helpers.js | 63 +- .../teams/js/spec/teams_tab_factory_spec.js | 10 +- .../teams/js/spec/views/edit_team_spec.js | 4 +- .../js/spec/views/instructor_tools_spec.js | 6 +- .../teams/js/spec/views/teams_tab_spec.js | 108 +- .../teams/js/spec/views/topic_teams_spec.js | 7 +- lms/static/js/spec/search/search_spec.js | 1281 +++++++++-------- 7 files changed, 758 insertions(+), 721 deletions(-) diff --git a/common/static/common/js/spec_helpers/page_helpers.js b/common/static/common/js/spec_helpers/page_helpers.js index da902d3dbd..033f0e9b6d 100644 --- a/common/static/common/js/spec_helpers/page_helpers.js +++ b/common/static/common/js/spec_helpers/page_helpers.js @@ -1,18 +1,49 @@ -define([ // jshint ignore:line -], -function() { - 'use strict'; - var getLocationHash; - /** - * Helper method that returns url hash. - * @return {String} Returns anchor part of current url. - */ - getLocationHash = function() { - return window.location.hash; - }; +define(["backbone"], + function(Backbone) { + 'use strict'; + var getLocationHash, preventBackboneChangingUrl; - return { - 'getLocationHash': getLocationHash - }; + /** + * Helper method that returns url hash. + * @return {String} Returns anchor part of current url. + */ + getLocationHash = function() { + return window.location.hash; + }; -}); + /** + * Prevent Backbone tests from changing the browser's URL. + * + * This function modifies Backbone so that tests can navigate + * without modifying the browser's URL. It works be adding + * stub versions of Backbone's hash functions so that updating + * the hash doesn't change the URL but instead updates a + * local object. The router's callbacks are still invoked + * so that to the test it appears that navigation is behaving + * as expected. + * + * Note: it is important that tests don't update the browser's + * URL because subsequent tests could find themselves in an + * unexpected navigation state. + */ + preventBackboneChangingUrl = function() { + var history = { + currentFragment: '' + }; + + // Stub out the Backbone router so that the browser doesn't actually navigate + spyOn(Backbone.history, '_updateHash').andCallFake(function (location, fragment, replace) { + history.currentFragment = fragment; + }); + + // Stub out getHash so that Backbone thinks that the browser has navigated + spyOn(Backbone.history, 'getHash').andCallFake(function () { + return history.currentFragment; + }); + }; + + return { + 'getLocationHash': getLocationHash, + 'preventBackboneChangingUrl': preventBackboneChangingUrl + }; + }); diff --git a/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js b/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js index ee4639e6a4..d41cfce9d5 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js @@ -1,6 +1,6 @@ define(['jquery', 'backbone', 'teams/js/teams_tab_factory', - 'teams/js/spec_helpers/team_spec_helpers'], - function($, Backbone, TeamsTabFactory, TeamSpecHelpers) { + 'common/js/spec_helpers/page_helpers', 'teams/js/spec_helpers/team_spec_helpers'], + function($, Backbone, TeamsTabFactory, PageHelpers, TeamSpecHelpers) { 'use strict'; describe("Teams Tab Factory", function() { @@ -10,6 +10,7 @@ define(['jquery', 'backbone', 'teams/js/teams_tab_factory', beforeEach(function() { setFixtures('
'); + PageHelpers.preventBackboneChangingUrl(); }); afterEach(function() { @@ -17,11 +18,6 @@ define(['jquery', 'backbone', 'teams/js/teams_tab_factory', }); it('can render the "Teams" tab', function() { - // Hack to make sure the URL fragments from earlier - // tests don't interfere with Backbone routing by the - // teams tab view - document.location.hash = ''; - initializeTeamsTabFactory(); expect($('.teams-content').text()).toContain('See all teams in your course, organized by topic'); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js index 843c5ce186..f2ddf34382 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js @@ -3,10 +3,11 @@ define([ 'underscore', 'backbone', 'common/js/spec_helpers/ajax_helpers', + 'common/js/spec_helpers/page_helpers', 'teams/js/views/edit_team', 'teams/js/models/team', 'teams/js/spec_helpers/team_spec_helpers' -], function ($, _, Backbone, AjaxHelpers, TeamEditView, TeamModel, TeamSpecHelpers) { +], function ($, _, Backbone, AjaxHelpers, PageHelpers, TeamEditView, TeamModel, TeamSpecHelpers) { 'use strict'; describe('CreateEditTeam', function() { @@ -90,6 +91,7 @@ define([ beforeEach(function () { setFixtures('
'); + PageHelpers.preventBackboneChangingUrl(); spyOn(Backbone.history, 'navigate'); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/instructor_tools_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/instructor_tools_spec.js index 6ae92dd048..12bb27d8d1 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/instructor_tools_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/instructor_tools_spec.js @@ -6,8 +6,9 @@ define([ 'teams/js/views/instructor_tools', 'teams/js/views/team_utils', 'teams/js/spec_helpers/team_spec_helpers', - 'common/js/spec_helpers/ajax_helpers' -], function ($, Backbone, _, Team, InstructorToolsView, TeamUtils, TeamSpecHelpers, AjaxHelpers) { + 'common/js/spec_helpers/ajax_helpers', + 'common/js/spec_helpers/page_helpers' +], function ($, Backbone, _, Team, InstructorToolsView, TeamUtils, TeamSpecHelpers, AjaxHelpers, PageHelpers) { 'use strict'; describe('Instructor Tools', function () { @@ -37,6 +38,7 @@ define([ beforeEach(function () { setFixtures('
'); + PageHelpers.preventBackboneChangingUrl(); spyOn(Backbone.history, 'navigate'); spyOn(TeamUtils, 'showMessage'); view = createInstructorTools().render(); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js index c43da45b7d..0665d50e28 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js @@ -3,13 +3,16 @@ define([ 'backbone', 'logger', 'common/js/spec_helpers/ajax_helpers', + 'common/js/spec_helpers/page_helpers', 'common/js/spec_helpers/spec_helpers', 'teams/js/views/teams_tab', 'teams/js/spec_helpers/team_spec_helpers' -], function ($, Backbone, Logger, AjaxHelpers, SpecHelpers, TeamsTabView, TeamSpecHelpers) { +], function ($, Backbone, Logger, AjaxHelpers, PageHelpers, SpecHelpers, TeamsTabView, TeamSpecHelpers) { 'use strict'; describe('TeamsTab', function() { + var requests; + var expectError = function (teamsTabView, text) { expect(teamsTabView.$('.warning').text()).toContain(text); }; @@ -18,13 +21,31 @@ define([ expect(element.focus).toHaveBeenCalled(); }; - var createTeamsTabView = function(options) { + var verifyTeamsRequest = function(options) { + AjaxHelpers.expectRequestURL(requests, TeamSpecHelpers.testContext.teamsUrl, + _.extend( + { + topic_id: TeamSpecHelpers.testTopicID, + expand: 'user', + course_id: TeamSpecHelpers.testCourseID, + order_by: '', + page: '1', + page_size: '10', + text_search: '' + }, + options + )); + }; + + var createTeamsTabView = function(test, options) { var teamsTabView = new TeamsTabView( { el: $('.teams-content'), context: TeamSpecHelpers.createMockContext(options) } ); + requests = AjaxHelpers.requests(test); + PageHelpers.preventBackboneChangingUrl(); teamsTabView.start(); return teamsTabView; }; @@ -39,7 +60,7 @@ define([ describe('Navigation', function () { it('does not render breadcrumbs for the top level tabs', function() { - var teamsTabView = createTeamsTabView(); + var teamsTabView = createTeamsTabView(this); teamsTabView.router.navigate('#my-teams', {trigger: true}); expect(teamsTabView.$('.breadcrumbs').length).toBe(0); teamsTabView.router.navigate('#browse', {trigger: true}); @@ -47,21 +68,20 @@ define([ }); it('does not interfere with anchor links to #content', function () { - var teamsTabView = createTeamsTabView(); + var teamsTabView = createTeamsTabView(this); teamsTabView.router.navigate('#content', {trigger: true}); expect(teamsTabView.$('.wrapper-msg')).toHaveClass('is-hidden'); }); it('displays and focuses an error message when trying to navigate to a nonexistent page', function () { - var teamsTabView = createTeamsTabView(); + var teamsTabView = createTeamsTabView(this); teamsTabView.router.navigate('no_such_page', {trigger: true}); expectError(teamsTabView, 'The page "no_such_page" could not be found.'); expectFocus(teamsTabView.$('.warning')); }); it('displays and focuses an error message when trying to navigate to a nonexistent topic', function () { - var requests = AjaxHelpers.requests(this), - teamsTabView = createTeamsTabView(); + var teamsTabView = createTeamsTabView(this); teamsTabView.router.navigate('topics/no_such_topic', {trigger: true}); AjaxHelpers.expectRequest(requests, 'GET', '/api/team/v0/topics/no_such_topic,course/1', null); AjaxHelpers.respondWithError(requests, 404); @@ -70,8 +90,7 @@ define([ }); it('displays and focuses an error message when trying to navigate to a nonexistent team', function () { - var requests = AjaxHelpers.requests(this), - teamsTabView = createTeamsTabView(); + var teamsTabView = createTeamsTabView(this); teamsTabView.router.navigate('teams/' + TeamSpecHelpers.testTopicID + '/no_such_team', {trigger: true}); AjaxHelpers.expectRequest(requests, 'GET', '/api/team/v0/teams/no_such_team?expand=user', null); AjaxHelpers.respondWithError(requests, 404); @@ -80,8 +99,7 @@ define([ }); it('displays and focuses an error message when it receives a 401 AJAX response', function () { - var requests = AjaxHelpers.requests(this), - teamsTabView = createTeamsTabView().render(); + var teamsTabView = createTeamsTabView(this).render(); teamsTabView.router.navigate('topics/' + TeamSpecHelpers.testTopicID, {trigger: true}); AjaxHelpers.respondWithError(requests, 401); expectError(teamsTabView, "Your request could not be completed. Reload the page and try again."); @@ -89,8 +107,7 @@ define([ }); it('displays and focuses an error message when it receives a 500 AJAX response', function () { - var requests = AjaxHelpers.requests(this), - teamsTabView = createTeamsTabView().render(); + var teamsTabView = createTeamsTabView(this).render(); teamsTabView.router.navigate('topics/' + TeamSpecHelpers.testTopicID, {trigger: true}); AjaxHelpers.respondWithError(requests, 500); expectError(teamsTabView, "Your request could not be completed due to a server problem. Reload the page and try again. If the issue persists, click the Help tab to report the problem."); @@ -98,7 +115,7 @@ define([ }); it('does not navigate to the topics page when syncing its collection if not on the search page', function () { - var teamsTabView = createTeamsTabView(), + var teamsTabView = createTeamsTabView(this), collection = TeamSpecHelpers.createMockTeams(); teamsTabView.createTeamsListView({ collection: collection, @@ -153,8 +170,7 @@ define([ } ] }, function (url, expectedEvent) { - var requests = AjaxHelpers.requests(this), - teamsTabView = createTeamsTabView({ + var teamsTabView = createTeamsTabView(this, { userInfo: TeamSpecHelpers.createMockUserInfo({staff: true}) }); teamsTabView.router.navigate(url, {trigger: true}); @@ -167,9 +183,9 @@ define([ describe('Discussion privileges', function () { it('allows privileged access to any team', function () { - var teamsTabView = createTeamsTabView({ - userInfo: TeamSpecHelpers.createMockUserInfo({privileged: true}) - }); + var teamsTabView = createTeamsTabView(this, { + userInfo: TeamSpecHelpers.createMockUserInfo({privileged: true}) + }); // Note: using `undefined` here to ensure that we // don't even look at the team when the user is // privileged @@ -177,12 +193,12 @@ define([ }); it('allows access to a team which an unprivileged user is a member of', function () { - var teamsTabView = createTeamsTabView({ - userInfo: TeamSpecHelpers.createMockUserInfo({ - username: TeamSpecHelpers.testUser, - privileged: false - }) - }); + var teamsTabView = createTeamsTabView(this, { + userInfo: TeamSpecHelpers.createMockUserInfo({ + username: TeamSpecHelpers.testUser, + privileged: false + }) + }); expect(teamsTabView.readOnlyDiscussion({ attributes: { membership: [{ @@ -195,9 +211,9 @@ define([ }); it('does not allow access if the user is neither privileged nor a team member', function () { - var teamsTabView = createTeamsTabView({ - userInfo: TeamSpecHelpers.createMockUserInfo({privileged: false, staff: true}) - }); + var teamsTabView = createTeamsTabView(this, { + userInfo: TeamSpecHelpers.createMockUserInfo({privileged: false, staff: true}) + }); expect(teamsTabView.readOnlyDiscussion({ attributes: {membership: []} })).toBe(true); @@ -205,25 +221,10 @@ define([ }); describe('Search', function () { - var verifyTeamsRequest = function(requests, options) { - AjaxHelpers.expectRequestURL(requests, TeamSpecHelpers.testContext.teamsUrl, - _.extend( - { - topic_id: TeamSpecHelpers.testTopicID, - expand: 'user', - course_id: TeamSpecHelpers.testCourseID, - order_by: '', - page: '1', - page_size: '10', - text_search: '' - }, - options - )); - }; var performSearch = function(requests, teamsTabView) { teamsTabView.$('.search-field').val('foo'); teamsTabView.$('.action-search').click(); - verifyTeamsRequest(requests, { + verifyTeamsRequest({ order_by: '', text_search: 'foo' }); @@ -231,11 +232,10 @@ define([ }; it('can search teams', function () { - var requests = AjaxHelpers.requests(this), - teamsTabView = createTeamsTabView(), + var teamsTabView = createTeamsTabView(this), requestCountBeforeSearch; teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); - verifyTeamsRequest(requests, { + verifyTeamsRequest({ order_by: 'last_activity_at', text_search: '' }); @@ -250,11 +250,9 @@ define([ }); it('can clear a search', function () { - var requests = AjaxHelpers.requests(this), - teamsTabView = createTeamsTabView(); + var teamsTabView = createTeamsTabView(this); teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); AjaxHelpers.respondWithJson(requests, {}); - performSearch(requests, teamsTabView); // Perform a search performSearch(requests, teamsTabView); @@ -262,7 +260,7 @@ define([ // Clear the search and submit it again teamsTabView.$('.search-field').val(''); teamsTabView.$('.action-search').click(); - verifyTeamsRequest(requests, { + verifyTeamsRequest({ order_by: 'last_activity_at', text_search: '' }); @@ -272,8 +270,7 @@ define([ }); it('can navigate back to all teams from a search', function () { - var requests = AjaxHelpers.requests(this), - teamsTabView = createTeamsTabView(); + var teamsTabView = createTeamsTabView(this); teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); AjaxHelpers.respondWithJson(requests, {}); @@ -283,7 +280,7 @@ define([ // Verify the breadcrumbs have a link back to the teams list, and click on it expect(teamsTabView.$('.breadcrumbs a').length).toBe(2); teamsTabView.$('.breadcrumbs a').last().click(); - verifyTeamsRequest(requests, { + verifyTeamsRequest({ order_by: 'last_activity_at', text_search: '' }); @@ -293,8 +290,7 @@ define([ }); it('does not switch to showing results when the search returns an error', function () { - var requests = AjaxHelpers.requests(this), - teamsTabView = createTeamsTabView(); + var teamsTabView = createTeamsTabView(this); teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); AjaxHelpers.respondWithJson(requests, {}); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js index ec08acfe1b..51708a9ee9 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js @@ -4,8 +4,10 @@ define([ 'teams/js/collections/team_membership', 'teams/js/views/topic_teams', 'teams/js/spec_helpers/team_spec_helpers', - 'common/js/spec_helpers/ajax_helpers' -], function (Backbone, TeamCollection, TeamMembershipCollection, TopicTeamsView, TeamSpecHelpers, AjaxHelpers) { + 'common/js/spec_helpers/ajax_helpers', + 'common/js/spec_helpers/page_helpers' +], function (Backbone, TeamCollection, TeamMembershipCollection, TopicTeamsView, TeamSpecHelpers, + AjaxHelpers, PageHelpers) { 'use strict'; describe('Topic Teams View', function () { var createTopicTeamsView = function(options) { @@ -39,6 +41,7 @@ define([ beforeEach(function () { setFixtures('
'); + PageHelpers.preventBackboneChangingUrl(); }); it('can render itself', function () { diff --git a/lms/static/js/spec/search/search_spec.js b/lms/static/js/spec/search/search_spec.js index e23edc94a3..e907d88072 100644 --- a/lms/static/js/spec/search/search_spec.js +++ b/lms/static/js/spec/search/search_spec.js @@ -3,6 +3,7 @@ define([ 'backbone', 'logger', 'common/js/spec_helpers/ajax_helpers', + 'common/js/spec_helpers/page_helpers', 'common/js/spec_helpers/template_helpers', 'js/search/base/models/search_result', 'js/search/base/collections/search_collection', @@ -20,6 +21,7 @@ define([ Backbone, Logger, AjaxHelpers, + PageHelpers, TemplateHelpers, SearchResult, SearchCollection, @@ -35,696 +37,534 @@ define([ ) { 'use strict'; - - describe('SearchResult', function () { - + describe('Search', function() { beforeEach(function () { - this.result = new SearchResult(); + PageHelpers.preventBackboneChangingUrl(); }); - it('has properties', function () { - expect(this.result.get('location')).toBeDefined(); - expect(this.result.get('content_type')).toBeDefined(); - expect(this.result.get('excerpt')).toBeDefined(); - expect(this.result.get('url')).toBeDefined(); - }); - - }); - - - describe('SearchCollection', function () { - - beforeEach(function () { - this.collection = new SearchCollection(); - - this.onSearch = jasmine.createSpy('onSearch'); - this.collection.on('search', this.onSearch); - - this.onNext = jasmine.createSpy('onNext'); - this.collection.on('next', this.onNext); - - this.onError = jasmine.createSpy('onError'); - this.collection.on('error', this.onError); - }); - - it('sends a request without a course ID', function () { - var collection = new SearchCollection([]); - spyOn($, 'ajax'); - collection.performSearch('search string'); - expect($.ajax.mostRecentCall.args[0].url).toEqual('/search/'); - }); - - it('sends a request with course ID', function () { - var collection = new SearchCollection([], { courseId: 'edx101' }); - spyOn($, 'ajax'); - collection.performSearch('search string'); - expect($.ajax.mostRecentCall.args[0].url).toEqual('/search/edx101'); - }); - - it('sends a request and parses the json result', function () { - var requests = AjaxHelpers.requests(this); - this.collection.performSearch('search string'); - var response = { - total: 2, - access_denied_count: 1, - results: [{ - data: { - location: ['section', 'subsection', 'unit'], - url: '/some/url/to/content', - content_type: 'text', - excerpt: 'this is a short excerpt' - } - }] - }; - AjaxHelpers.respondWithJson(requests, response); - - expect(this.onSearch).toHaveBeenCalled(); - expect(this.collection.totalCount).toEqual(1); - expect(this.collection.latestModelsCount).toEqual(1); - expect(this.collection.accessDeniedCount).toEqual(1); - expect(this.collection.page).toEqual(0); - expect(this.collection.first().attributes).toEqual(response.results[0].data); - }); - - it('handles errors', function () { - var requests = AjaxHelpers.requests(this); - this.collection.performSearch('search string'); - AjaxHelpers.respondWithError(requests, 500); - expect(this.onSearch).not.toHaveBeenCalled(); - expect(this.onError).toHaveBeenCalled(); - }); - - it('loads next page', function () { - var requests = AjaxHelpers.requests(this); - var response = { total: 35, results: [] }; - this.collection.loadNextPage(); - AjaxHelpers.respondWithJson(requests, response); - expect(this.onNext).toHaveBeenCalled(); - expect(this.onError).not.toHaveBeenCalled(); - }); - - it('sends correct paging parameters', function () { - var requests = AjaxHelpers.requests(this); - var searchString = 'search string'; - var response = { total: 52, results: [] }; - this.collection.performSearch(searchString); - AjaxHelpers.respondWithJson(requests, response); - this.collection.loadNextPage(); - AjaxHelpers.respondWithJson(requests, response); - spyOn($, 'ajax'); - this.collection.loadNextPage(); - expect($.ajax.mostRecentCall.args[0].url).toEqual(this.collection.url); - expect($.ajax.mostRecentCall.args[0].data.search_string).toEqual(searchString); - expect($.ajax.mostRecentCall.args[0].data.page_size).toEqual(this.collection.pageSize); - expect($.ajax.mostRecentCall.args[0].data.page_index).toEqual(2); - }); - - it('has next page', function () { - var requests = AjaxHelpers.requests(this); - var response = { total: 35, access_denied_count: 5, results: [] }; - this.collection.performSearch('search string'); - AjaxHelpers.respondWithJson(requests, response); - expect(this.collection.hasNextPage()).toEqual(true); - this.collection.loadNextPage(); - AjaxHelpers.respondWithJson(requests, response); - expect(this.collection.hasNextPage()).toEqual(false); - }); - - it('aborts any previous request', function () { - var requests = AjaxHelpers.requests(this); - var response = { total: 35, results: [] }; - - this.collection.performSearch('old search'); - this.collection.performSearch('new search'); - AjaxHelpers.respondWithJson(requests, response); - expect(this.onSearch.calls.length).toEqual(1); - - this.collection.performSearch('old search'); - this.collection.cancelSearch(); - AjaxHelpers.respondWithJson(requests, response); - expect(this.onSearch.calls.length).toEqual(1); - - this.collection.loadNextPage(); - this.collection.loadNextPage(); - AjaxHelpers.respondWithJson(requests, response); - expect(this.onNext.calls.length).toEqual(1); - }); - - describe('reset state', function () { + describe('SearchResult', function () { beforeEach(function () { - this.collection.page = 2; - this.collection.totalCount = 35; - this.collection.latestModelsCount = 5; + this.result = new SearchResult(); }); - it('resets state when performing new search', function () { + it('has properties', function () { + expect(this.result.get('location')).toBeDefined(); + expect(this.result.get('content_type')).toBeDefined(); + expect(this.result.get('excerpt')).toBeDefined(); + expect(this.result.get('url')).toBeDefined(); + }); + + }); + + + describe('SearchCollection', function () { + + beforeEach(function () { + this.collection = new SearchCollection(); + + this.onSearch = jasmine.createSpy('onSearch'); + this.collection.on('search', this.onSearch); + + this.onNext = jasmine.createSpy('onNext'); + this.collection.on('next', this.onNext); + + this.onError = jasmine.createSpy('onError'); + this.collection.on('error', this.onError); + }); + + it('sends a request without a course ID', function () { + var collection = new SearchCollection([]); + spyOn($, 'ajax'); + collection.performSearch('search string'); + expect($.ajax.mostRecentCall.args[0].url).toEqual('/search/'); + }); + + it('sends a request with course ID', function () { + var collection = new SearchCollection([], { courseId: 'edx101' }); + spyOn($, 'ajax'); + collection.performSearch('search string'); + expect($.ajax.mostRecentCall.args[0].url).toEqual('/search/edx101'); + }); + + it('sends a request and parses the json result', function () { + var requests = AjaxHelpers.requests(this); this.collection.performSearch('search string'); - expect(this.collection.models.length).toEqual(0); + var response = { + total: 2, + access_denied_count: 1, + results: [{ + data: { + location: ['section', 'subsection', 'unit'], + url: '/some/url/to/content', + content_type: 'text', + excerpt: 'this is a short excerpt' + } + }] + }; + AjaxHelpers.respondWithJson(requests, response); + + expect(this.onSearch).toHaveBeenCalled(); + expect(this.collection.totalCount).toEqual(1); + expect(this.collection.latestModelsCount).toEqual(1); + expect(this.collection.accessDeniedCount).toEqual(1); expect(this.collection.page).toEqual(0); - expect(this.collection.totalCount).toEqual(0); - expect(this.collection.latestModelsCount).toEqual(0); + expect(this.collection.first().attributes).toEqual(response.results[0].data); }); - it('resets state when canceling a search', function () { + it('handles errors', function () { + var requests = AjaxHelpers.requests(this); + this.collection.performSearch('search string'); + AjaxHelpers.respondWithError(requests, 500); + expect(this.onSearch).not.toHaveBeenCalled(); + expect(this.onError).toHaveBeenCalled(); + }); + + it('loads next page', function () { + var requests = AjaxHelpers.requests(this); + var response = { total: 35, results: [] }; + this.collection.loadNextPage(); + AjaxHelpers.respondWithJson(requests, response); + expect(this.onNext).toHaveBeenCalled(); + expect(this.onError).not.toHaveBeenCalled(); + }); + + it('sends correct paging parameters', function () { + var requests = AjaxHelpers.requests(this); + var searchString = 'search string'; + var response = { total: 52, results: [] }; + this.collection.performSearch(searchString); + AjaxHelpers.respondWithJson(requests, response); + this.collection.loadNextPage(); + AjaxHelpers.respondWithJson(requests, response); + spyOn($, 'ajax'); + this.collection.loadNextPage(); + expect($.ajax.mostRecentCall.args[0].url).toEqual(this.collection.url); + expect($.ajax.mostRecentCall.args[0].data.search_string).toEqual(searchString); + expect($.ajax.mostRecentCall.args[0].data.page_size).toEqual(this.collection.pageSize); + expect($.ajax.mostRecentCall.args[0].data.page_index).toEqual(2); + }); + + it('has next page', function () { + var requests = AjaxHelpers.requests(this); + var response = { total: 35, access_denied_count: 5, results: [] }; + this.collection.performSearch('search string'); + AjaxHelpers.respondWithJson(requests, response); + expect(this.collection.hasNextPage()).toEqual(true); + this.collection.loadNextPage(); + AjaxHelpers.respondWithJson(requests, response); + expect(this.collection.hasNextPage()).toEqual(false); + }); + + it('aborts any previous request', function () { + var requests = AjaxHelpers.requests(this); + var response = { total: 35, results: [] }; + + this.collection.performSearch('old search'); + this.collection.performSearch('new search'); + AjaxHelpers.respondWithJson(requests, response); + expect(this.onSearch.calls.length).toEqual(1); + + this.collection.performSearch('old search'); this.collection.cancelSearch(); - expect(this.collection.models.length).toEqual(0); - expect(this.collection.page).toEqual(0); - expect(this.collection.totalCount).toEqual(0); - expect(this.collection.latestModelsCount).toEqual(0); + AjaxHelpers.respondWithJson(requests, response); + expect(this.onSearch.calls.length).toEqual(1); + + this.collection.loadNextPage(); + this.collection.loadNextPage(); + AjaxHelpers.respondWithJson(requests, response); + expect(this.onNext.calls.length).toEqual(1); + }); + + describe('reset state', function () { + + beforeEach(function () { + this.collection.page = 2; + this.collection.totalCount = 35; + this.collection.latestModelsCount = 5; + }); + + it('resets state when performing new search', function () { + this.collection.performSearch('search string'); + expect(this.collection.models.length).toEqual(0); + expect(this.collection.page).toEqual(0); + expect(this.collection.totalCount).toEqual(0); + expect(this.collection.latestModelsCount).toEqual(0); + }); + + it('resets state when canceling a search', function () { + this.collection.cancelSearch(); + expect(this.collection.models.length).toEqual(0); + expect(this.collection.page).toEqual(0); + expect(this.collection.totalCount).toEqual(0); + expect(this.collection.latestModelsCount).toEqual(0); + }); + }); }); - }); + describe('SearchRouter', function () { - describe('SearchRouter', function () { - - beforeEach(function () { - this.router = new SearchRouter(); - this.onSearch = jasmine.createSpy('onSearch'); - this.router.on('search', this.onSearch); - }); - - it ('has a search route', function () { - expect(this.router.routes['search/:query']).toEqual('search'); - }); - - it ('triggers a search event', function () { - var query = 'mercury'; - this.router.search(query); - expect(this.onSearch).toHaveBeenCalledWith(query); - }); - - }); - - - describe('SearchItemView', function () { - - function beforeEachHelper(SearchItemView) { - TemplateHelpers.installTemplates([ - 'templates/search/course_search_item', - 'templates/search/dashboard_search_item' - ]); - - this.model = new SearchResult({ - location: ['section', 'subsection', 'unit'], - content_type: 'Video', - course_name: 'Course Name', - excerpt: 'A short excerpt.', - url: 'path/to/content' - }); - - this.seqModel = new SearchResult({ - location: ['section', 'subsection'], - content_type: 'Sequence', - course_name: 'Course Name', - excerpt: 'A short excerpt.', - url: 'path/to/content' - }); - - this.item = new SearchItemView({ model: this.model }); - this.item.render(); - this.seqItem = new SearchItemView({ model: this.seqModel }); - this.seqItem.render(); - } - - function rendersItem() { - expect(this.item.$el).toHaveAttr('role', 'region'); - expect(this.item.$el).toHaveAttr('aria-label', 'search result'); - expect(this.item.$el).toContain('a[href="' + this.model.get('url') + '"]'); - expect(this.item.$el.find('.result-type')).toContainHtml(this.model.get('content_type')); - expect(this.item.$el.find('.result-excerpt')).toContainHtml(this.model.get('excerpt')); - expect(this.item.$el.find('.result-location')).toContainHtml('section ▸ subsection ▸ unit'); - } - - function rendersSequentialItem() { - expect(this.seqItem.$el).toHaveAttr('role', 'region'); - expect(this.seqItem.$el).toHaveAttr('aria-label', 'search result'); - expect(this.seqItem.$el).toContain('a[href="' + this.seqModel.get('url') + '"]'); - expect(this.seqItem.$el.find('.result-type')).toBeEmpty(); - expect(this.seqItem.$el.find('.result-excerpt')).toBeEmpty(); - expect(this.seqItem.$el.find('.result-location')).toContainHtml('section ▸ subsection'); - } - - function logsSearchItemViewEvent() { - this.model.collection = new SearchCollection([this.model], { course_id: 'edx101' }); - this.item.render(); - // Mock the redirect call - spyOn(this.item, 'redirect').andCallFake( function() {} ); - spyOn(Logger, 'log').andReturn($.Deferred().resolve()); - this.item.$el.find('a').trigger('click'); - expect(this.item.redirect).toHaveBeenCalled(); - this.item.$el.trigger('click'); - expect(this.item.redirect).toHaveBeenCalled(); - } - - describe('CourseSearchItemView', function () { beforeEach(function () { - beforeEachHelper.call(this, CourseSearchItemView); - }); - it('renders items correctly', rendersItem); - it('renders Sequence items correctly', rendersSequentialItem); - it('logs view event', logsSearchItemViewEvent); - }); - - describe('DashSearchItemView', function () { - beforeEach(function () { - beforeEachHelper.call(this, DashSearchItemView); - }); - it('renders items correctly', rendersItem); - it('renders Sequence items correctly', rendersSequentialItem); - it('displays course name in breadcrumbs', function () { - expect(this.seqItem.$el.find('.result-course-name')).toContainHtml(this.model.get('course_name')); - }); - it('logs view event', logsSearchItemViewEvent); - }); - - }); - - - describe('SearchForm', function () { - - function trimsInputString() { - var term = ' search string '; - $('.search-field').val(term); - $('form').trigger('submit'); - expect(this.onSearch).toHaveBeenCalledWith($.trim(term)); - } - - function doesSearch() { - var term = ' search string '; - $('.search-field').val(term); - this.form.doSearch(term); - expect(this.onSearch).toHaveBeenCalledWith($.trim(term)); - expect($('.search-field').val()).toEqual(term); - expect($('.search-field')).toHaveClass('is-active'); - expect($('.search-button')).toBeHidden(); - expect($('.cancel-button')).toBeVisible(); - } - - function triggersSearchEvent() { - var term = 'search string'; - $('.search-field').val(term); - $('form').trigger('submit'); - expect(this.onSearch).toHaveBeenCalledWith(term); - expect($('.search-field')).toHaveClass('is-active'); - expect($('.search-button')).toBeHidden(); - expect($('.cancel-button')).toBeVisible(); - } - - function clearsSearchOnCancel() { - $('.search-field').val('search string'); - $('.search-button').trigger('click'); - $('.cancel-button').trigger('click'); - expect($('.search-field')).not.toHaveClass('is-active'); - expect($('.search-button')).toBeVisible(); - expect($('.cancel-button')).toBeHidden(); - expect($('.search-field')).toHaveValue(''); - } - - function clearsSearchOnEmpty() { - $('.search-field').val(''); - $('form').trigger('submit'); - expect(this.onClear).toHaveBeenCalled(); - expect($('.search-field')).not.toHaveClass('is-active'); - expect($('.cancel-button')).toBeHidden(); - expect($('.search-button')).toBeVisible(); - } - - describe('CourseSearchForm', function () { - beforeEach(function () { - loadFixtures('js/fixtures/search/course_search_form.html'); - this.form = new CourseSearchForm(); - this.onClear = jasmine.createSpy('onClear'); + this.router = new SearchRouter(); this.onSearch = jasmine.createSpy('onSearch'); - this.form.on('clear', this.onClear); - this.form.on('search', this.onSearch); + this.router.on('search', this.onSearch); }); - it('trims input string', trimsInputString); - it('handles calls to doSearch', doesSearch); - it('triggers a search event and changes to active state', triggersSearchEvent); - it('clears search when clicking on cancel button', clearsSearchOnCancel); - it('clears search when search box is empty', clearsSearchOnEmpty); + + it ('has a search route', function () { + expect(this.router.routes['search/:query']).toEqual('search'); + }); + + it ('triggers a search event', function () { + var query = 'mercury'; + this.router.search(query); + expect(this.onSearch).toHaveBeenCalledWith(query); + }); + }); - describe('DashSearchForm', function () { - beforeEach(function () { - loadFixtures('js/fixtures/search/dashboard_search_form.html'); - this.form = new DashSearchForm(); - this.onClear = jasmine.createSpy('onClear'); - this.onSearch = jasmine.createSpy('onSearch'); - this.form.on('clear', this.onClear); - this.form.on('search', this.onSearch); + + describe('SearchItemView', function () { + + function beforeEachHelper(SearchItemView) { + TemplateHelpers.installTemplates([ + 'templates/search/course_search_item', + 'templates/search/dashboard_search_item' + ]); + + this.model = new SearchResult({ + location: ['section', 'subsection', 'unit'], + content_type: 'Video', + course_name: 'Course Name', + excerpt: 'A short excerpt.', + url: 'path/to/content' + }); + + this.seqModel = new SearchResult({ + location: ['section', 'subsection'], + content_type: 'Sequence', + course_name: 'Course Name', + excerpt: 'A short excerpt.', + url: 'path/to/content' + }); + + this.item = new SearchItemView({ model: this.model }); + this.item.render(); + this.seqItem = new SearchItemView({ model: this.seqModel }); + this.seqItem.render(); + } + + function rendersItem() { + expect(this.item.$el).toHaveAttr('role', 'region'); + expect(this.item.$el).toHaveAttr('aria-label', 'search result'); + expect(this.item.$el).toContain('a[href="' + this.model.get('url') + '"]'); + expect(this.item.$el.find('.result-type')).toContainHtml(this.model.get('content_type')); + expect(this.item.$el.find('.result-excerpt')).toContainHtml(this.model.get('excerpt')); + expect(this.item.$el.find('.result-location')).toContainHtml('section ▸ subsection ▸ unit'); + } + + function rendersSequentialItem() { + expect(this.seqItem.$el).toHaveAttr('role', 'region'); + expect(this.seqItem.$el).toHaveAttr('aria-label', 'search result'); + expect(this.seqItem.$el).toContain('a[href="' + this.seqModel.get('url') + '"]'); + expect(this.seqItem.$el.find('.result-type')).toBeEmpty(); + expect(this.seqItem.$el.find('.result-excerpt')).toBeEmpty(); + expect(this.seqItem.$el.find('.result-location')).toContainHtml('section ▸ subsection'); + } + + function logsSearchItemViewEvent() { + this.model.collection = new SearchCollection([this.model], { course_id: 'edx101' }); + this.item.render(); + // Mock the redirect call + spyOn(this.item, 'redirect').andCallFake( function() {} ); + spyOn(Logger, 'log').andReturn($.Deferred().resolve()); + this.item.$el.find('a').trigger('click'); + expect(this.item.redirect).toHaveBeenCalled(); + this.item.$el.trigger('click'); + expect(this.item.redirect).toHaveBeenCalled(); + } + + describe('CourseSearchItemView', function () { + beforeEach(function () { + beforeEachHelper.call(this, CourseSearchItemView); + }); + it('renders items correctly', rendersItem); + it('renders Sequence items correctly', rendersSequentialItem); + it('logs view event', logsSearchItemViewEvent); }); - it('trims input string', trimsInputString); - it('handles calls to doSearch', doesSearch); - it('triggers a search event and changes to active state', triggersSearchEvent); - it('clears search when clicking on cancel button', clearsSearchOnCancel); - it('clears search when search box is empty', clearsSearchOnEmpty); + + describe('DashSearchItemView', function () { + beforeEach(function () { + beforeEachHelper.call(this, DashSearchItemView); + }); + it('renders items correctly', rendersItem); + it('renders Sequence items correctly', rendersSequentialItem); + it('displays course name in breadcrumbs', function () { + expect(this.seqItem.$el.find('.result-course-name')).toContainHtml(this.model.get('course_name')); + }); + it('logs view event', logsSearchItemViewEvent); + }); + }); - }); + describe('SearchForm', function () { - describe('SearchResultsView', function () { + function trimsInputString() { + var term = ' search string '; + $('.search-field').val(term); + $('form').trigger('submit'); + expect(this.onSearch).toHaveBeenCalledWith($.trim(term)); + } - function showsLoadingMessage () { - this.resultsView.showLoadingMessage(); - expect(this.resultsView.$contentElement).toBeHidden(); - expect(this.resultsView.$el).toBeVisible(); - expect(this.resultsView.$el).not.toBeEmpty(); - } + function doesSearch() { + var term = ' search string '; + $('.search-field').val(term); + this.form.doSearch(term); + expect(this.onSearch).toHaveBeenCalledWith($.trim(term)); + expect($('.search-field').val()).toEqual(term); + expect($('.search-field')).toHaveClass('is-active'); + expect($('.search-button')).toBeHidden(); + expect($('.cancel-button')).toBeVisible(); + } - function showsErrorMessage () { - this.resultsView.showErrorMessage(); - expect(this.resultsView.$contentElement).toBeHidden(); - expect(this.resultsView.$el).toBeVisible(); - expect(this.resultsView.$el).not.toBeEmpty(); - } + function triggersSearchEvent() { + var term = 'search string'; + $('.search-field').val(term); + $('form').trigger('submit'); + expect(this.onSearch).toHaveBeenCalledWith(term); + expect($('.search-field')).toHaveClass('is-active'); + expect($('.search-button')).toBeHidden(); + expect($('.cancel-button')).toBeVisible(); + } - function returnsToContent () { - this.resultsView.clear(); - expect(this.resultsView.$contentElement).toBeVisible(); - expect(this.resultsView.$el).toBeHidden(); - expect(this.resultsView.$el).toBeEmpty(); - } + function clearsSearchOnCancel() { + $('.search-field').val('search string'); + $('.search-button').trigger('click'); + $('.cancel-button').trigger('click'); + expect($('.search-field')).not.toHaveClass('is-active'); + expect($('.search-button')).toBeVisible(); + expect($('.cancel-button')).toBeHidden(); + expect($('.search-field')).toHaveValue(''); + } - function showsNoResultsMessage() { - this.collection.reset(); - this.resultsView.render(); - expect(this.resultsView.$el).toContainHtml('no results'); - expect(this.resultsView.$el.find('ol')).not.toExist(); - } + function clearsSearchOnEmpty() { + $('.search-field').val(''); + $('form').trigger('submit'); + expect(this.onClear).toHaveBeenCalled(); + expect($('.search-field')).not.toHaveClass('is-active'); + expect($('.cancel-button')).toBeHidden(); + expect($('.search-button')).toBeVisible(); + } - function rendersSearchResults () { - var searchResults = [{ - location: ['section', 'subsection', 'unit'], - url: '/some/url/to/content', - content_type: 'text', - course_name: '', - excerpt: 'this is a short excerpt' - }]; - this.collection.set(searchResults); - this.collection.latestModelsCount = 1; - this.collection.totalCount = 1; - - this.resultsView.render(); - expect(this.resultsView.$el.find('ol')[0]).toExist(); - expect(this.resultsView.$el.find('li').length).toEqual(1); - expect(this.resultsView.$el).toContainHtml('Search Results'); - expect(this.resultsView.$el).toContainHtml('this is a short excerpt'); - - this.collection.set(searchResults); - this.collection.totalCount = 2; - this.resultsView.renderNext(); - expect(this.resultsView.$el.find('.search-count')).toContainHtml('2'); - expect(this.resultsView.$el.find('li').length).toEqual(2); - } - - function showsMoreResultsLink () { - this.collection.totalCount = 123; - this.collection.hasNextPage = function () { return true; }; - this.resultsView.render(); - expect(this.resultsView.$el.find('a.search-load-next')[0]).toExist(); - - this.collection.totalCount = 123; - this.collection.hasNextPage = function () { return false; }; - this.resultsView.render(); - expect(this.resultsView.$el.find('a.search-load-next')[0]).not.toExist(); - } - - function triggersNextPageEvent () { - var onNext = jasmine.createSpy('onNext'); - this.resultsView.on('next', onNext); - this.collection.totalCount = 123; - this.collection.hasNextPage = function () { return true; }; - this.resultsView.render(); - this.resultsView.$el.find('a.search-load-next').click(); - expect(onNext).toHaveBeenCalled(); - } - - function showsLoadMoreSpinner () { - this.collection.totalCount = 123; - this.collection.hasNextPage = function () { return true; }; - this.resultsView.render(); - expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden(); - this.resultsView.loadNext(); - // toBeVisible does not work with inline - expect(this.resultsView.$el.find('a.search-load-next .icon')).toHaveCss({ 'display': 'inline' }); - this.resultsView.renderNext(); - expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden(); - } - - function beforeEachHelper(SearchResultsView) { - appendSetFixtures( - '
' + - '
' + - '
' + - '
' - ); - - TemplateHelpers.installTemplates([ - 'templates/search/course_search_item', - 'templates/search/dashboard_search_item', - 'templates/search/course_search_results', - 'templates/search/dashboard_search_results', - 'templates/search/search_list', - 'templates/search/search_loading', - 'templates/search/search_error' - ]); - - var MockCollection = Backbone.Collection.extend({ - hasNextPage: function () {}, - latestModelsCount: 0, - pageSize: 20, - latestModels: function () { - return SearchCollection.prototype.latestModels.apply(this, arguments); - } + describe('CourseSearchForm', function () { + beforeEach(function () { + loadFixtures('js/fixtures/search/course_search_form.html'); + this.form = new CourseSearchForm(); + this.onClear = jasmine.createSpy('onClear'); + this.onSearch = jasmine.createSpy('onSearch'); + this.form.on('clear', this.onClear); + this.form.on('search', this.onSearch); + }); + it('trims input string', trimsInputString); + it('handles calls to doSearch', doesSearch); + it('triggers a search event and changes to active state', triggersSearchEvent); + it('clears search when clicking on cancel button', clearsSearchOnCancel); + it('clears search when search box is empty', clearsSearchOnEmpty); }); - this.collection = new MockCollection(); - this.resultsView = new SearchResultsView({ collection: this.collection }); - } - describe('CourseSearchResultsView', function () { - beforeEach(function() { - beforeEachHelper.call(this, CourseSearchResultsView); + describe('DashSearchForm', function () { + beforeEach(function () { + loadFixtures('js/fixtures/search/dashboard_search_form.html'); + this.form = new DashSearchForm(); + this.onClear = jasmine.createSpy('onClear'); + this.onSearch = jasmine.createSpy('onSearch'); + this.form.on('clear', this.onClear); + this.form.on('search', this.onSearch); + }); + it('trims input string', trimsInputString); + it('handles calls to doSearch', doesSearch); + it('triggers a search event and changes to active state', triggersSearchEvent); + it('clears search when clicking on cancel button', clearsSearchOnCancel); + it('clears search when search box is empty', clearsSearchOnEmpty); }); - it('shows loading message', showsLoadingMessage); - it('shows error message', showsErrorMessage); - it('returns to content', returnsToContent); - it('shows a message when there are no results', showsNoResultsMessage); - it('renders search results', rendersSearchResults); - it('shows a link to load more results', showsMoreResultsLink); - it('triggers an event for next page', triggersNextPageEvent); - it('shows a spinner when loading more results', showsLoadMoreSpinner); + }); - describe('DashSearchResultsView', function () { - beforeEach(function() { - beforeEachHelper.call(this, DashSearchResultsView); - }); - it('shows loading message', showsLoadingMessage); - it('shows error message', showsErrorMessage); - it('returns to content', returnsToContent); - it('shows a message when there are no results', showsNoResultsMessage); - it('renders search results', rendersSearchResults); - it('shows a link to load more results', showsMoreResultsLink); - it('triggers an event for next page', triggersNextPageEvent); - it('shows a spinner when loading more results', showsLoadMoreSpinner); - it('returns back to courses', function () { - var onReset = jasmine.createSpy('onReset'); - this.resultsView.on('reset', onReset); - this.resultsView.render(); - expect(this.resultsView.$el.find('a.search-back-to-courses')).toExist(); - this.resultsView.$el.find('.search-back-to-courses').click(); - expect(onReset).toHaveBeenCalled(); + + describe('SearchResultsView', function () { + + function showsLoadingMessage () { + this.resultsView.showLoadingMessage(); + expect(this.resultsView.$contentElement).toBeHidden(); + expect(this.resultsView.$el).toBeVisible(); + expect(this.resultsView.$el).not.toBeEmpty(); + } + + function showsErrorMessage () { + this.resultsView.showErrorMessage(); + expect(this.resultsView.$contentElement).toBeHidden(); + expect(this.resultsView.$el).toBeVisible(); + expect(this.resultsView.$el).not.toBeEmpty(); + } + + function returnsToContent () { + this.resultsView.clear(); expect(this.resultsView.$contentElement).toBeVisible(); expect(this.resultsView.$el).toBeHidden(); - }); - }); + expect(this.resultsView.$el).toBeEmpty(); + } - }); + function showsNoResultsMessage() { + this.collection.reset(); + this.resultsView.render(); + expect(this.resultsView.$el).toContainHtml('no results'); + expect(this.resultsView.$el.find('ol')).not.toExist(); + } + function rendersSearchResults () { + var searchResults = [{ + location: ['section', 'subsection', 'unit'], + url: '/some/url/to/content', + content_type: 'text', + course_name: '', + excerpt: 'this is a short excerpt' + }]; + this.collection.set(searchResults); + this.collection.latestModelsCount = 1; + this.collection.totalCount = 1; - describe('SearchApp', function () { + this.resultsView.render(); + expect(this.resultsView.$el.find('ol')[0]).toExist(); + expect(this.resultsView.$el.find('li').length).toEqual(1); + expect(this.resultsView.$el).toContainHtml('Search Results'); + expect(this.resultsView.$el).toContainHtml('this is a short excerpt'); - function showsLoadingMessage () { - $('.search-field').val('search string'); - $('.search-button').trigger('click'); - expect(this.$contentElement).toBeHidden(); - expect(this.$searchResults).toBeVisible(); - expect(this.$searchResults).not.toBeEmpty(); - } + this.collection.set(searchResults); + this.collection.totalCount = 2; + this.resultsView.renderNext(); + expect(this.resultsView.$el.find('.search-count')).toContainHtml('2'); + expect(this.resultsView.$el.find('li').length).toEqual(2); + } - function performsSearch () { - var requests = AjaxHelpers.requests(this); - $('.search-field').val('search string'); - $('.search-button').trigger('click'); - AjaxHelpers.respondWithJson(requests, { - total: 1337, - access_denied_count: 12, - results: [{ - data: { - location: ['section', 'subsection', 'unit'], - url: '/some/url/to/content', - content_type: 'text', - excerpt: 'this is a short excerpt', - course_name: '' - } - }] - }); - expect($('.search-info')).toExist(); - expect($('.search-result-list')).toBeVisible(); - expect(this.$searchResults.find('li').length).toEqual(1); - } + function showsMoreResultsLink () { + this.collection.totalCount = 123; + this.collection.hasNextPage = function () { return true; }; + this.resultsView.render(); + expect(this.resultsView.$el.find('a.search-load-next')[0]).toExist(); - function showsErrorMessage () { - var requests = AjaxHelpers.requests(this); - $('.search-field').val('search string'); - $('.search-button').trigger('click'); - AjaxHelpers.respondWithError(requests, 500, {}); - expect(this.$searchResults).toContainHtml('There was an error'); - } + this.collection.totalCount = 123; + this.collection.hasNextPage = function () { return false; }; + this.resultsView.render(); + expect(this.resultsView.$el.find('a.search-load-next')[0]).not.toExist(); + } - function updatesNavigationHistory () { - $('.search-field').val('edx'); - $('.search-button').trigger('click'); - expect(Backbone.history.navigate.calls[0].args).toContain('search/edx'); - $('.cancel-button').trigger('click'); - expect(Backbone.history.navigate.calls[1].args).toContain(''); - } + function triggersNextPageEvent () { + var onNext = jasmine.createSpy('onNext'); + this.resultsView.on('next', onNext); + this.collection.totalCount = 123; + this.collection.hasNextPage = function () { return true; }; + this.resultsView.render(); + this.resultsView.$el.find('a.search-load-next').click(); + expect(onNext).toHaveBeenCalled(); + } - function cancelsSearchRequest () { - var requests = AjaxHelpers.requests(this); - // send search request to server - $('.search-field').val('search string'); - $('.search-button').trigger('click'); - // cancel search - $('.cancel-button').trigger('click'); - AjaxHelpers.respondWithJson(requests, { - total: 1337, - access_denied_count: 12, - results: [{ - data: { - location: ['section', 'subsection', 'unit'], - url: '/some/url/to/content', - content_type: 'text', - excerpt: 'this is a short excerpt', - course_name: '' - } - }] - }); - // there should be no results - expect(this.$contentElement).toBeVisible(); - expect(this.$searchResults).toBeHidden(); - } + function showsLoadMoreSpinner () { + this.collection.totalCount = 123; + this.collection.hasNextPage = function () { return true; }; + this.resultsView.render(); + expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden(); + this.resultsView.loadNext(); + // toBeVisible does not work with inline + expect(this.resultsView.$el.find('a.search-load-next .icon')).toHaveCss({ 'display': 'inline' }); + this.resultsView.renderNext(); + expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden(); + } - function clearsResults () { - $('.cancel-button').trigger('click'); - expect(this.$contentElement).toBeVisible(); - expect(this.$searchResults).toBeHidden(); - } - - function loadsNextPage () { - var requests = AjaxHelpers.requests(this); - var response = { - total: 1337, - access_denied_count: 12, - results: [{ - data: { - location: ['section', 'subsection', 'unit'], - url: '/some/url/to/content', - content_type: 'text', - excerpt: 'this is a short excerpt', - course_name: '' - } - }] - }; - $('.search-field').val('query'); - $('.search-button').trigger('click'); - AjaxHelpers.respondWithJson(requests, response); - expect(this.$searchResults.find('li').length).toEqual(1); - expect($('.search-load-next')).toBeVisible(); - $('.search-load-next').trigger('click'); - var body = requests[1].requestBody; - expect(body).toContain('search_string=query'); - expect(body).toContain('page_index=1'); - AjaxHelpers.respondWithJson(requests, response); - expect(this.$searchResults.find('li').length).toEqual(2); - } - - function navigatesToSearch () { - var requests = AjaxHelpers.requests(this); - Backbone.history.loadUrl('search/query'); - expect(requests[0].requestBody).toContain('search_string=query'); - } - - function loadTemplates () { - TemplateHelpers.installTemplates([ - 'templates/search/course_search_item', - 'templates/search/dashboard_search_item', - 'templates/search/search_loading', - 'templates/search/search_error', - 'templates/search/course_search_results', - 'templates/search/dashboard_search_results' - ]); - } - - describe('CourseSearchApp', function () { - - beforeEach(function () { - loadFixtures('js/fixtures/search/course_search_form.html'); + function beforeEachHelper(SearchResultsView) { appendSetFixtures( '
' + - '
' - ); - loadTemplates.call(this); - - var courseId = 'a/b/c'; - CourseSearchFactory(courseId); - spyOn(Backbone.history, 'navigate'); - this.$contentElement = $('#course-content'); - this.$searchResults = $('#courseware-search-results'); - }); - - it('shows loading message on search', showsLoadingMessage); - it('performs search', performsSearch); - it('shows an error message', showsErrorMessage); - it('updates navigation history', updatesNavigationHistory); - it('cancels search request', cancelsSearchRequest); - it('clears results', clearsResults); - it('loads next page', loadsNextPage); - it('navigates to search', navigatesToSearch); - - }); - - describe('DashSearchApp', function () { - - beforeEach(function () { - loadFixtures('js/fixtures/search/dashboard_search_form.html'); - appendSetFixtures( + '
' + '
' + '
' ); - loadTemplates.call(this); - DashboardSearchFactory(); - spyOn(Backbone.history, 'navigate'); - this.$contentElement = $('#my-courses'); - this.$searchResults = $('#dashboard-search-results'); + TemplateHelpers.installTemplates([ + 'templates/search/course_search_item', + 'templates/search/dashboard_search_item', + 'templates/search/course_search_results', + 'templates/search/dashboard_search_results', + 'templates/search/search_list', + 'templates/search/search_loading', + 'templates/search/search_error' + ]); + + var MockCollection = Backbone.Collection.extend({ + hasNextPage: function () {}, + latestModelsCount: 0, + pageSize: 20, + latestModels: function () { + return SearchCollection.prototype.latestModels.apply(this, arguments); + } + }); + this.collection = new MockCollection(); + this.resultsView = new SearchResultsView({ collection: this.collection }); + } + + describe('CourseSearchResultsView', function () { + beforeEach(function() { + beforeEachHelper.call(this, CourseSearchResultsView); + }); + it('shows loading message', showsLoadingMessage); + it('shows error message', showsErrorMessage); + it('returns to content', returnsToContent); + it('shows a message when there are no results', showsNoResultsMessage); + it('renders search results', rendersSearchResults); + it('shows a link to load more results', showsMoreResultsLink); + it('triggers an event for next page', triggersNextPageEvent); + it('shows a spinner when loading more results', showsLoadMoreSpinner); }); - it('shows loading message on search', showsLoadingMessage); - it('performs search', performsSearch); - it('shows an error message', showsErrorMessage); - it('updates navigation history', updatesNavigationHistory); - it('cancels search request', cancelsSearchRequest); - it('clears results', clearsResults); - it('loads next page', loadsNextPage); - it('navigates to search', navigatesToSearch); - it('returns to course list', function () { + describe('DashSearchResultsView', function () { + beforeEach(function() { + beforeEachHelper.call(this, DashSearchResultsView); + }); + it('shows loading message', showsLoadingMessage); + it('shows error message', showsErrorMessage); + it('returns to content', returnsToContent); + it('shows a message when there are no results', showsNoResultsMessage); + it('renders search results', rendersSearchResults); + it('shows a link to load more results', showsMoreResultsLink); + it('triggers an event for next page', triggersNextPageEvent); + it('shows a spinner when loading more results', showsLoadMoreSpinner); + it('returns back to courses', function () { + var onReset = jasmine.createSpy('onReset'); + this.resultsView.on('reset', onReset); + this.resultsView.render(); + expect(this.resultsView.$el.find('a.search-back-to-courses')).toExist(); + this.resultsView.$el.find('.search-back-to-courses').click(); + expect(onReset).toHaveBeenCalled(); + expect(this.resultsView.$contentElement).toBeVisible(); + expect(this.resultsView.$el).toBeHidden(); + }); + }); + + }); + + + describe('SearchApp', function () { + + function showsLoadingMessage () { + $('.search-field').val('search string'); + $('.search-button').trigger('click'); + expect(this.$contentElement).toBeHidden(); + expect(this.$searchResults).toBeVisible(); + expect(this.$searchResults).not.toBeEmpty(); + } + + function performsSearch () { var requests = AjaxHelpers.requests(this); $('.search-field').val('search string'); $('.search-button').trigger('click'); @@ -741,15 +581,182 @@ define([ } }] }); - expect($('.search-back-to-courses')).toExist(); - $('.search-back-to-courses').trigger('click'); + expect($('.search-info')).toExist(); + expect($('.search-result-list')).toBeVisible(); + expect(this.$searchResults.find('li').length).toEqual(1); + } + + function showsErrorMessage () { + var requests = AjaxHelpers.requests(this); + $('.search-field').val('search string'); + $('.search-button').trigger('click'); + AjaxHelpers.respondWithError(requests, 500, {}); + expect(this.$searchResults).toContainHtml('There was an error'); + } + + function updatesNavigationHistory () { + $('.search-field').val('edx'); + $('.search-button').trigger('click'); + expect(Backbone.history.navigate.calls[0].args).toContain('search/edx'); + $('.cancel-button').trigger('click'); + expect(Backbone.history.navigate.calls[1].args).toContain(''); + } + + function cancelsSearchRequest () { + var requests = AjaxHelpers.requests(this); + // send search request to server + $('.search-field').val('search string'); + $('.search-button').trigger('click'); + // cancel search + $('.cancel-button').trigger('click'); + AjaxHelpers.respondWithJson(requests, { + total: 1337, + access_denied_count: 12, + results: [{ + data: { + location: ['section', 'subsection', 'unit'], + url: '/some/url/to/content', + content_type: 'text', + excerpt: 'this is a short excerpt', + course_name: '' + } + }] + }); + // there should be no results expect(this.$contentElement).toBeVisible(); expect(this.$searchResults).toBeHidden(); - expect(this.$searchResults).toBeEmpty(); + } + + function clearsResults () { + $('.cancel-button').trigger('click'); + expect(this.$contentElement).toBeVisible(); + expect(this.$searchResults).toBeHidden(); + } + + function loadsNextPage () { + var requests = AjaxHelpers.requests(this); + var response = { + total: 1337, + access_denied_count: 12, + results: [{ + data: { + location: ['section', 'subsection', 'unit'], + url: '/some/url/to/content', + content_type: 'text', + excerpt: 'this is a short excerpt', + course_name: '' + } + }] + }; + $('.search-field').val('query'); + $('.search-button').trigger('click'); + AjaxHelpers.respondWithJson(requests, response); + expect(this.$searchResults.find('li').length).toEqual(1); + expect($('.search-load-next')).toBeVisible(); + $('.search-load-next').trigger('click'); + var body = requests[1].requestBody; + expect(body).toContain('search_string=query'); + expect(body).toContain('page_index=1'); + AjaxHelpers.respondWithJson(requests, response); + expect(this.$searchResults.find('li').length).toEqual(2); + } + + function navigatesToSearch () { + var requests = AjaxHelpers.requests(this); + Backbone.history.loadUrl('search/query'); + expect(requests[0].requestBody).toContain('search_string=query'); + } + + function loadTemplates () { + TemplateHelpers.installTemplates([ + 'templates/search/course_search_item', + 'templates/search/dashboard_search_item', + 'templates/search/search_loading', + 'templates/search/search_error', + 'templates/search/course_search_results', + 'templates/search/dashboard_search_results' + ]); + } + + describe('CourseSearchApp', function () { + + beforeEach(function () { + loadFixtures('js/fixtures/search/course_search_form.html'); + appendSetFixtures( + '
' + + '
' + ); + loadTemplates.call(this); + + var courseId = 'a/b/c'; + CourseSearchFactory(courseId); + spyOn(Backbone.history, 'navigate'); + this.$contentElement = $('#course-content'); + this.$searchResults = $('#courseware-search-results'); + }); + + it('shows loading message on search', showsLoadingMessage); + it('performs search', performsSearch); + it('shows an error message', showsErrorMessage); + it('updates navigation history', updatesNavigationHistory); + it('cancels search request', cancelsSearchRequest); + it('clears results', clearsResults); + it('loads next page', loadsNextPage); + it('navigates to search', navigatesToSearch); + + }); + + describe('DashSearchApp', function () { + + beforeEach(function () { + loadFixtures('js/fixtures/search/dashboard_search_form.html'); + appendSetFixtures( + '
' + + '
' + ); + loadTemplates.call(this); + DashboardSearchFactory(); + + spyOn(Backbone.history, 'navigate'); + this.$contentElement = $('#my-courses'); + this.$searchResults = $('#dashboard-search-results'); + }); + + it('shows loading message on search', showsLoadingMessage); + it('performs search', performsSearch); + it('shows an error message', showsErrorMessage); + it('updates navigation history', updatesNavigationHistory); + it('cancels search request', cancelsSearchRequest); + it('clears results', clearsResults); + it('loads next page', loadsNextPage); + it('navigates to search', navigatesToSearch); + it('returns to course list', function () { + var requests = AjaxHelpers.requests(this); + $('.search-field').val('search string'); + $('.search-button').trigger('click'); + AjaxHelpers.respondWithJson(requests, { + total: 1337, + access_denied_count: 12, + results: [{ + data: { + location: ['section', 'subsection', 'unit'], + url: '/some/url/to/content', + content_type: 'text', + excerpt: 'this is a short excerpt', + course_name: '' + } + }] + }); + expect($('.search-back-to-courses')).toExist(); + $('.search-back-to-courses').trigger('click'); + expect(this.$contentElement).toBeVisible(); + expect(this.$searchResults).toBeHidden(); + expect(this.$searchResults).toBeEmpty(); + }); + }); }); }); - });