From ad16f9f968fd2a0d8a144759c88d8d45d60e38d2 Mon Sep 17 00:00:00 2001
From: cahrens <christina@edx.org>
Date: Thu, 2 Jul 2015 09:20:27 -0400
Subject: [PATCH] Optimize search usages.

---
 lms/envs/common.py                            |  17 -
 lms/static/js/discovery/app.js                |  71 --
 lms/static/js/discovery/discovery_factory.js  |  73 ++
 lms/static/js/discovery/main.js               |  22 -
 .../js/search/course/course_search_factory.js |  52 ++
 lms/static/js/search/course/main.js           |  22 -
 lms/static/js/search/course/search_app.js     |  50 --
 .../dashboard/dashboard_search_factory.js     |  56 ++
 lms/static/js/search/dashboard/main.js        |  19 -
 lms/static/js/search/dashboard/search_app.js  |  54 --
 .../discovery/course_discovery_meanings.js    |  19 -
 .../js/spec/discovery/discovery_spec.js       | 835 +++++++++---------
 lms/static/js/spec/main.js                    |   4 +-
 lms/static/js/spec/search/search_spec.js      |  23 +-
 lms/static/lms/js/build.js                    |   5 +-
 lms/templates/courseware/courses.html         |  24 +-
 lms/templates/courseware/courseware.html      |   5 +-
 lms/templates/dashboard.html                  |   4 +-
 18 files changed, 635 insertions(+), 720 deletions(-)
 delete mode 100644 lms/static/js/discovery/app.js
 create mode 100644 lms/static/js/discovery/discovery_factory.js
 delete mode 100644 lms/static/js/discovery/main.js
 create mode 100644 lms/static/js/search/course/course_search_factory.js
 delete mode 100644 lms/static/js/search/course/main.js
 delete mode 100644 lms/static/js/search/course/search_app.js
 create mode 100644 lms/static/js/search/dashboard/dashboard_search_factory.js
 delete mode 100644 lms/static/js/search/dashboard/main.js
 delete mode 100644 lms/static/js/search/dashboard/search_app.js
 delete mode 100644 lms/static/js/spec/discovery/course_discovery_meanings.js

diff --git a/lms/envs/common.py b/lms/envs/common.py
index 510b97eb263..0189b7dbc75 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1212,8 +1212,6 @@ courseware_js = (
     sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.js'))
 )
 
-courseware_search_js = ['js/search/course/main.js']
-
 
 # Before a student accesses courseware, we do not
 # need many of the JS dependencies.  This includes
@@ -1252,7 +1250,6 @@ base_application_js = [
 dashboard_js = (
     sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/dashboard/**/*.js'))
 )
-dashboard_search_js = ['js/search/dashboard/main.js']
 discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js'))
 staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.js'))
 open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_ended/**/*.js'))
@@ -1338,8 +1335,6 @@ incourse_reverify_js = [
 
 ccx_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/ccx/**/*.js'))
 
-discovery_js = ['js/discovery/main.js']
-
 certificates_web_view_js = [
     'js/vendor/jquery.min.js',
     'js/vendor/jquery.cookie.js',
@@ -1504,10 +1499,6 @@ PIPELINE_JS = {
         'source_filenames': courseware_js,
         'output_filename': 'js/lms-courseware.js',
     },
-    'courseware_search': {
-        'source_filenames': courseware_search_js,
-        'output_filename': 'js/lms-courseware-search.js',
-    },
     'base_vendor': {
         'source_filenames': base_vendor_js,
         'output_filename': 'js/lms-base-vendor.js',
@@ -1548,10 +1539,6 @@ PIPELINE_JS = {
         'source_filenames': dashboard_js,
         'output_filename': 'js/dashboard.js'
     },
-    'dashboard_search': {
-        'source_filenames': dashboard_search_js,
-        'output_filename': 'js/dashboard-search.js',
-    },
     'student_account': {
         'source_filenames': student_account_js,
         'output_filename': 'js/student_account.js'
@@ -1576,10 +1563,6 @@ PIPELINE_JS = {
         'source_filenames': ['js/footer-edx.js'],
         'output_filename': 'js/footer-edx.js'
     },
-    'discovery': {
-        'source_filenames': discovery_js,
-        'output_filename': 'js/discovery.js'
-    },
     'certificates_wv': {
         'source_filenames': certificates_web_view_js,
         'output_filename': 'js/certificates/web_view.js'
diff --git a/lms/static/js/discovery/app.js b/lms/static/js/discovery/app.js
deleted file mode 100644
index 7e9a89ac59a..00000000000
--- a/lms/static/js/discovery/app.js
+++ /dev/null
@@ -1,71 +0,0 @@
-;(function (define) {
-
-define(['backbone', 'course_discovery_meanings'], function(Backbone, meanings) {
-    'use strict';
-
-    return function (Collection, Form, ResultListView, FilterBarView, FacetsBarView, searchQuery) {
-        //facet types configuration - set default display names
-        var facetsTypes = meanings;
-
-        var collection = new Collection([]);
-        var results = new ResultListView({ collection: collection });
-        var dispatcher = _.clone(Backbone.Events);
-        var form = new Form();
-        var filters = new FilterBarView();
-        var facetsBarView = new FacetsBarView(facetsTypes);
-
-        dispatcher.listenTo(form, 'search', function (query) {
-            form.showLoadingIndicator();
-            filters.changeQueryFilter(query);
-        });
-
-        dispatcher.listenTo(filters, 'search', function (searchTerm, facets) {
-            collection.performSearch(searchTerm, facets);
-            form.showLoadingIndicator();
-        });
-
-        dispatcher.listenTo(filters, 'clear', function () {
-            form.clearSearch();
-            collection.performSearch();
-            filters.hideClearAllButton();
-        });
-
-        dispatcher.listenTo(results, 'next', function () {
-            collection.loadNextPage();
-            form.showLoadingIndicator();
-        });
-
-        dispatcher.listenTo(collection, 'search', function () {
-            if (collection.length > 0) {
-                form.showFoundMessage(collection.totalCount);
-                results.render();
-            }
-            else {
-                form.showNotFoundMessage(collection.searchTerm);
-            }
-            facetsBarView.renderFacets(collection.facets);
-            form.hideLoadingIndicator();
-        });
-
-        dispatcher.listenTo(collection, 'next', function () {
-            results.renderNext();
-            form.hideLoadingIndicator();
-        });
-
-        dispatcher.listenTo(collection, 'error', function () {
-            form.showErrorMessage();
-            form.hideLoadingIndicator();
-        });
-
-        dispatcher.listenTo(facetsBarView, 'addFilter', function (data) {
-            filters.addFilter(data);
-        });
-
-        // kick off search on page refresh
-        form.doSearch(searchQuery);
-
-    };
-
-});
-
-})(define || RequireJS.define);
diff --git a/lms/static/js/discovery/discovery_factory.js b/lms/static/js/discovery/discovery_factory.js
new file mode 100644
index 00000000000..adfd8d08a6f
--- /dev/null
+++ b/lms/static/js/discovery/discovery_factory.js
@@ -0,0 +1,73 @@
+;(function (define) {
+    'use strict';
+
+    define(['backbone', 'js/discovery/collection', 'js/discovery/form', 'js/discovery/result_list_view',
+            'js/discovery/filter_bar_view', 'js/discovery/search_facets_view'],
+        function(Backbone, Collection, Form, ResultListView, FilterBarView, FacetsBarView) {
+
+            return function (meanings, searchQuery) {
+                //facet types configuration - set default display names
+                var facetsTypes = meanings;
+
+                var collection = new Collection([]);
+                var results = new ResultListView({ collection: collection });
+                var dispatcher = _.clone(Backbone.Events);
+                var form = new Form();
+                var filters = new FilterBarView();
+                var facetsBarView = new FacetsBarView(facetsTypes);
+
+                dispatcher.listenTo(form, 'search', function (query) {
+                    form.showLoadingIndicator();
+                    filters.changeQueryFilter(query);
+                });
+
+                dispatcher.listenTo(filters, 'search', function (searchTerm, facets) {
+                    collection.performSearch(searchTerm, facets);
+                    form.showLoadingIndicator();
+                });
+
+                dispatcher.listenTo(filters, 'clear', function () {
+                    form.clearSearch();
+                    collection.performSearch();
+                    filters.hideClearAllButton();
+                });
+
+                dispatcher.listenTo(results, 'next', function () {
+                    collection.loadNextPage();
+                    form.showLoadingIndicator();
+                });
+
+                dispatcher.listenTo(collection, 'search', function () {
+                    if (collection.length > 0) {
+                        form.showFoundMessage(collection.totalCount);
+                        results.render();
+                    }
+                    else {
+                        form.showNotFoundMessage(collection.searchTerm);
+                    }
+                    facetsBarView.renderFacets(collection.facets);
+                    form.hideLoadingIndicator();
+                });
+
+                dispatcher.listenTo(collection, 'next', function () {
+                    results.renderNext();
+                    form.hideLoadingIndicator();
+                });
+
+                dispatcher.listenTo(collection, 'error', function () {
+                    form.showErrorMessage();
+                    form.hideLoadingIndicator();
+                });
+
+                dispatcher.listenTo(facetsBarView, 'addFilter', function (data) {
+                    filters.addFilter(data);
+                });
+
+                // kick off search on page refresh
+                form.doSearch(searchQuery);
+
+            };
+
+        });
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/discovery/main.js b/lms/static/js/discovery/main.js
deleted file mode 100644
index 7325c215689..00000000000
--- a/lms/static/js/discovery/main.js
+++ /dev/null
@@ -1,22 +0,0 @@
-RequireJS.require([
-    'jquery',
-    'backbone',
-    'js/discovery/app',
-    'js/discovery/collection',
-    'js/discovery/form',
-    'js/discovery/result_list_view',
-    'js/discovery/filter_bar_view',
-    'js/discovery/search_facets_view'
-], function ($, Backbone, App, Collection, DiscoveryForm, ResultListView, FilterBarView, FacetsBarView) {
-    'use strict';
-
-    var app = new App(
-        Collection,
-        DiscoveryForm,
-        ResultListView,
-        FilterBarView,
-        FacetsBarView,
-        getParameterByName('search_query')
-    );
-
-});
diff --git a/lms/static/js/search/course/course_search_factory.js b/lms/static/js/search/course/course_search_factory.js
new file mode 100644
index 00000000000..4f2a68d9167
--- /dev/null
+++ b/lms/static/js/search/course/course_search_factory.js
@@ -0,0 +1,52 @@
+;(function (define) {
+    'use strict';
+
+    define(['backbone', 'js/search/base/routers/search_router', 'js/search/course/views/search_form',
+            'js/search/base/collections/search_collection', 'js/search/course/views/search_results_view'],
+        function(Backbone, SearchRouter, CourseSearchForm, SearchCollection, SearchResultsView) {
+
+            return function (courseId) {
+
+                var router = new SearchRouter();
+                var form = new CourseSearchForm();
+                var collection = new SearchCollection([], { courseId: courseId });
+                var results = new SearchResultsView({ collection: collection });
+                var dispatcher = _.clone(Backbone.Events);
+
+                dispatcher.listenTo(router, 'search', function (query) {
+                    form.doSearch(query);
+                });
+
+                dispatcher.listenTo(form, 'search', function (query) {
+                    results.showLoadingMessage();
+                    collection.performSearch(query);
+                    router.navigate('search/' + query, { replace: true });
+                });
+
+                dispatcher.listenTo(form, 'clear', function () {
+                    collection.cancelSearch();
+                    results.clear();
+                    router.navigate('');
+                });
+
+                dispatcher.listenTo(results, 'next', function () {
+                    collection.loadNextPage();
+                });
+
+                dispatcher.listenTo(collection, 'search', function () {
+                    results.render();
+                });
+
+                dispatcher.listenTo(collection, 'next', function () {
+                    results.renderNext();
+                });
+
+                dispatcher.listenTo(collection, 'error', function () {
+                    results.showErrorMessage();
+                });
+
+            };
+
+        });
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/course/main.js b/lms/static/js/search/course/main.js
deleted file mode 100644
index 9d8bd2759ca..00000000000
--- a/lms/static/js/search/course/main.js
+++ /dev/null
@@ -1,22 +0,0 @@
-RequireJS.require([
-    'jquery',
-    'backbone',
-    'js/search/course/search_app',
-    'js/search/base/routers/search_router',
-    'js/search/course/views/search_form',
-    'js/search/base/collections/search_collection',
-    'js/search/course/views/search_results_view'
-], function ($, Backbone, SearchApp, SearchRouter, CourseSearchForm, SearchCollection, CourseSearchResultsView) {
-    'use strict';
-
-    var courseId = $('#courseware-search-results').data('courseId');
-    var app = new SearchApp(
-        courseId,
-        SearchRouter,
-        CourseSearchForm,
-        SearchCollection,
-        CourseSearchResultsView
-    );
-    Backbone.history.start();
-
-});
diff --git a/lms/static/js/search/course/search_app.js b/lms/static/js/search/course/search_app.js
deleted file mode 100644
index 992c5fdf560..00000000000
--- a/lms/static/js/search/course/search_app.js
+++ /dev/null
@@ -1,50 +0,0 @@
-;(function (define) {
-
-define(['backbone'], function(Backbone) {
-    'use strict';
-
-    return function (courseId, SearchRouter, SearchForm, SearchCollection, SearchListView) {
-
-        var router = new SearchRouter();
-        var form = new SearchForm();
-        var collection = new SearchCollection([], { courseId: courseId });
-        var results = new SearchListView({ collection: collection });
-        var dispatcher = _.clone(Backbone.Events);
-
-        dispatcher.listenTo(router, 'search', function (query) {
-            form.doSearch(query);
-        });
-
-        dispatcher.listenTo(form, 'search', function (query) {
-            results.showLoadingMessage();
-            collection.performSearch(query);
-            router.navigate('search/' + query, { replace: true });
-        });
-
-        dispatcher.listenTo(form, 'clear', function () {
-            collection.cancelSearch();
-            results.clear();
-            router.navigate('');
-        });
-
-        dispatcher.listenTo(results, 'next', function () {
-            collection.loadNextPage();
-        });
-
-        dispatcher.listenTo(collection, 'search', function () {
-            results.render();
-        });
-
-        dispatcher.listenTo(collection, 'next', function () {
-            results.renderNext();
-        });
-
-        dispatcher.listenTo(collection, 'error', function () {
-            results.showErrorMessage();
-        });
-
-    };
-
-});
-
-})(define || RequireJS.define);
diff --git a/lms/static/js/search/dashboard/dashboard_search_factory.js b/lms/static/js/search/dashboard/dashboard_search_factory.js
new file mode 100644
index 00000000000..f44215c416c
--- /dev/null
+++ b/lms/static/js/search/dashboard/dashboard_search_factory.js
@@ -0,0 +1,56 @@
+;(function (define) {
+    'use strict';
+
+    define(['backbone', 'js/search/base/routers/search_router', 'js/search/dashboard/views/search_form',
+            'js/search/base/collections/search_collection', 'js/search/dashboard/views/search_results_view'],
+        function(Backbone, SearchRouter, SearchForm, SearchCollection, SearchListView) {
+
+            return function () {
+
+                var router = new SearchRouter();
+                var form = new SearchForm();
+                var collection = new SearchCollection([]);
+                var results = new SearchListView({ collection: collection });
+                var dispatcher = _.clone(Backbone.Events);
+
+                dispatcher.listenTo(router, 'search', function (query) {
+                    form.doSearch(query);
+                });
+
+                dispatcher.listenTo(form, 'search', function (query) {
+                    results.showLoadingMessage();
+                    collection.performSearch(query);
+                    router.navigate('search/' + query, { replace: true });
+                });
+
+                dispatcher.listenTo(form, 'clear', function () {
+                    collection.cancelSearch();
+                    results.clear();
+                    router.navigate('');
+                });
+
+                dispatcher.listenTo(results, 'next', function () {
+                    collection.loadNextPage();
+                });
+
+                dispatcher.listenTo(results, 'reset', function () {
+                    form.resetSearchForm();
+                });
+
+                dispatcher.listenTo(collection, 'search', function () {
+                    results.render();
+                });
+
+                dispatcher.listenTo(collection, 'next', function () {
+                    results.renderNext();
+                });
+
+                dispatcher.listenTo(collection, 'error', function () {
+                    results.showErrorMessage();
+                });
+
+            };
+
+        });
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/dashboard/main.js b/lms/static/js/search/dashboard/main.js
deleted file mode 100644
index 0a6574caa04..00000000000
--- a/lms/static/js/search/dashboard/main.js
+++ /dev/null
@@ -1,19 +0,0 @@
-RequireJS.require([
-    'backbone',
-    'js/search/dashboard/search_app',
-    'js/search/base/routers/search_router',
-    'js/search/dashboard/views/search_form',
-    'js/search/base/collections/search_collection',
-    'js/search/dashboard/views/search_results_view'
-], function (Backbone, SearchApp, SearchRouter, DashSearchForm, SearchCollection, DashSearchResultsView) {
-    'use strict';
-
-    var app = new SearchApp(
-        SearchRouter,
-        DashSearchForm,
-        SearchCollection,
-        DashSearchResultsView
-    );
-    Backbone.history.start();
-
-});
diff --git a/lms/static/js/search/dashboard/search_app.js b/lms/static/js/search/dashboard/search_app.js
deleted file mode 100644
index d60d6047502..00000000000
--- a/lms/static/js/search/dashboard/search_app.js
+++ /dev/null
@@ -1,54 +0,0 @@
-;(function (define) {
-
-define(['backbone'], function(Backbone) {
-    'use strict';
-
-    return function (SearchRouter, SearchForm, SearchCollection, SearchListView) {
-
-        var router = new SearchRouter();
-        var form = new SearchForm();
-        var collection = new SearchCollection([]);
-        var results = new SearchListView({ collection: collection });
-        var dispatcher = _.clone(Backbone.Events);
-
-        dispatcher.listenTo(router, 'search', function (query) {
-            form.doSearch(query);
-        });
-
-        dispatcher.listenTo(form, 'search', function (query) {
-            results.showLoadingMessage();
-            collection.performSearch(query);
-            router.navigate('search/' + query, { replace: true });
-        });
-
-        dispatcher.listenTo(form, 'clear', function () {
-            collection.cancelSearch();
-            results.clear();
-            router.navigate('');
-        });
-
-        dispatcher.listenTo(results, 'next', function () {
-            collection.loadNextPage();
-        });
-
-        dispatcher.listenTo(results, 'reset', function () {
-            form.resetSearchForm();
-        });
-
-        dispatcher.listenTo(collection, 'search', function () {
-            results.render();
-        });
-
-        dispatcher.listenTo(collection, 'next', function () {
-            results.renderNext();
-        });
-
-        dispatcher.listenTo(collection, 'error', function () {
-            results.showErrorMessage();
-        });
-
-    };
-
-});
-
-})(define || RequireJS.define);
diff --git a/lms/static/js/spec/discovery/course_discovery_meanings.js b/lms/static/js/spec/discovery/course_discovery_meanings.js
deleted file mode 100644
index e468d553a77..00000000000
--- a/lms/static/js/spec/discovery/course_discovery_meanings.js
+++ /dev/null
@@ -1,19 +0,0 @@
-define({
-    org: {
-        name: 'Organization',
-        terms: {
-            edX1: "edX_1"
-        }
-    },
-    modes: {
-        name: 'Course Type',
-        terms: {
-            honor: 'Honor',
-            verified: 'Verified'
-        }
-    },
-    language: {
-        en: 'English',
-        hr: 'Croatian'
-    }
-});
diff --git a/lms/static/js/spec/discovery/discovery_spec.js b/lms/static/js/spec/discovery/discovery_spec.js
index be06997d4cd..ccbe8b7ee61 100644
--- a/lms/static/js/spec/discovery/discovery_spec.js
+++ b/lms/static/js/spec/discovery/discovery_spec.js
@@ -4,7 +4,7 @@ define([
     'logger',
     'common/js/spec_helpers/ajax_helpers',
     'common/js/spec_helpers/template_helpers',
-    'js/discovery/app',
+    'js/discovery/discovery_factory',
     'js/discovery/collection',
     'js/discovery/form',
     'js/discovery/result',
@@ -14,16 +14,14 @@ define([
     'js/discovery/filters',
     'js/discovery/filter_bar_view',
     'js/discovery/filter_view',
-    'js/discovery/search_facets_view',
-    'js/discovery/facet_view',
-    'js/discovery/facets_view'
+    'js/discovery/search_facets_view'
 ], function(
     $,
     Backbone,
     Logger,
     AjaxHelpers,
     TemplateHelpers,
-    App,
+    DiscoveryFactory,
     Collection,
     DiscoveryForm,
     ResultItem,
@@ -33,9 +31,7 @@ define([
     FiltersCollection,
     FiltersBarView,
     FilterView,
-    SearchFacetView,
-    FacetView,
-    FacetsView
+    SearchFacetView
 ) {
     'use strict';
 
@@ -113,126 +109,116 @@ define([
     var SEARCH_FILTER = {"type": "search_string", "query": "search3"};
 
 
-    describe('Collection', function () {
+    describe('Course Discovery', function () {
 
-        beforeEach(function () {
-            this.collection = new Collection();
+        describe('Collection', function () {
 
-            this.onSearch = jasmine.createSpy('onSearch');
-            this.collection.on('search', this.onSearch);
+            beforeEach(function () {
+                this.collection = new Collection();
 
-            this.onNext = jasmine.createSpy('onNext');
-            this.collection.on('next', this.onNext);
+                this.onSearch = jasmine.createSpy('onSearch');
+                this.collection.on('search', this.onSearch);
 
-            this.onError = jasmine.createSpy('onError');
-            this.collection.on('error', this.onError);
-        });
-
-        it('sends a request and parses the json result', function () {
-            var requests = AjaxHelpers.requests(this);
-            this.collection.performSearch('search string');
-            AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
-            expect(this.onSearch).toHaveBeenCalled();
-            expect(this.collection.totalCount).toEqual(365);
-            expect(this.collection.latestModels()[0].attributes).toEqual(JSON_RESPONSE.results[0].data);
-            expect(this.collection.page).toEqual(0);
-        });
+                this.onNext = jasmine.createSpy('onNext');
+                this.collection.on('next', this.onNext);
 
-        it('handles errors', function () {
-            var requests = AjaxHelpers.requests(this);
-            this.collection.performSearch('search string');
-            AjaxHelpers.respondWithError(requests);
-            expect(this.onSearch).not.toHaveBeenCalled();
-            expect(this.onError).toHaveBeenCalled();
-            this.collection.loadNextPage();
-            AjaxHelpers.respondWithError(requests);
-            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 response = { total: 52, results: [] };
-            this.collection.performSearch('search string');
-            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).toEqual({
-                search_string : 'search string',
-                page_size : this.collection.pageSize,
-                page_index : 2
+                this.onError = jasmine.createSpy('onError');
+                this.collection.on('error', this.onError);
             });
-        });
 
-        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('sends a request and parses the json result', function () {
+                var requests = AjaxHelpers.requests(this);
+                this.collection.performSearch('search string');
+                AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
+                expect(this.onSearch).toHaveBeenCalled();
+                expect(this.collection.totalCount).toEqual(365);
+                expect(this.collection.latestModels()[0].attributes).toEqual(JSON_RESPONSE.results[0].data);
+                expect(this.collection.page).toEqual(0);
+            });
 
-        it('resets state when performing new search', function () {
-            this.collection.add(new ResultItem());
-            expect(this.collection.length).toEqual(1);
-            this.collection.performSearch('search string');
-            expect(this.collection.length).toEqual(0);
-            expect(this.collection.page).toEqual(0);
-            expect(this.collection.totalCount).toEqual(0);
-            expect(this.collection.latestModelsCount).toEqual(0);
-        });
+            it('handles errors', function () {
+                var requests = AjaxHelpers.requests(this);
+                this.collection.performSearch('search string');
+                AjaxHelpers.respondWithError(requests);
+                expect(this.onSearch).not.toHaveBeenCalled();
+                expect(this.onError).toHaveBeenCalled();
+                this.collection.loadNextPage();
+                AjaxHelpers.respondWithError(requests);
+                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 response = { total: 52, results: [] };
+                this.collection.performSearch('search string');
+                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).toEqual({
+                    search_string : 'search string',
+                    page_size : this.collection.pageSize,
+                    page_index : 2
+                });
+            });
 
-    describe('ResultItem', function () {
+            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);
+            });
 
-        beforeEach(function () {
-            this.result = new ResultItem();
-        });
+            it('resets state when performing new search', function () {
+                this.collection.add(new ResultItem());
+                expect(this.collection.length).toEqual(1);
+                this.collection.performSearch('search string');
+                expect(this.collection.length).toEqual(0);
+                expect(this.collection.page).toEqual(0);
+                expect(this.collection.totalCount).toEqual(0);
+                expect(this.collection.latestModelsCount).toEqual(0);
+            });
 
-        it('has properties', function () {
-            expect(this.result.get('modes')).toBeDefined();
-            expect(this.result.get('course')).toBeDefined();
-            expect(this.result.get('enrollment_start')).toBeDefined();
-            expect(this.result.get('number')).toBeDefined();
-            expect(this.result.get('content')).toEqual({
-                display_name: '',
-                number: '',
-                overview: ''
-            });
-            expect(this.result.get('start')).toBeDefined();
-            expect(this.result.get('image_url')).toBeDefined();
-            expect(this.result.get('org')).toBeDefined();
-            expect(this.result.get('id')).toBeDefined();
         });
 
-    });
 
+        describe('ResultItem', function () {
 
-    describe('ResultItemView', function () {
+            beforeEach(function () {
+                this.result = new ResultItem();
+            });
 
-        beforeEach(function () {
-            TemplateHelpers.installTemplate('templates/discovery/result_item');
-            this.item = new ResultItemView({
-                model: new ResultItem(JSON_RESPONSE.results[0].data)
+            it('has properties', function () {
+                expect(this.result.get('modes')).toBeDefined();
+                expect(this.result.get('course')).toBeDefined();
+                expect(this.result.get('enrollment_start')).toBeDefined();
+                expect(this.result.get('number')).toBeDefined();
+                expect(this.result.get('content')).toEqual({
+                    display_name: '',
+                    number: '',
+                    overview: ''
+                });
+                expect(this.result.get('start')).toBeDefined();
+                expect(this.result.get('image_url')).toBeDefined();
+                expect(this.result.get('org')).toBeDefined();
+                expect(this.result.get('id')).toBeDefined();
             });
-        });
 
         it('renders correctly', function () {
             var data = this.item.model.attributes;
@@ -246,350 +232,383 @@ define([
             expect(this.item.$el.find('.course-date')).toContainHtml('Jan 01, 1970');
         });
 
-    });
 
+        describe('ResultItemView', function () {
 
-    describe('DiscoveryForm', function () {
+            beforeEach(function () {
+                TemplateHelpers.installTemplate('templates/discovery/result_item');
+                this.item = new ResultItemView({
+                    model: new ResultItem(JSON_RESPONSE.results[0].data)
+                });
+            });
 
-        beforeEach(function () {
-            loadFixtures('js/fixtures/discovery.html');
-            this.form = new DiscoveryForm();
-            this.onSearch = jasmine.createSpy('onSearch');
-            this.form.on('search', this.onSearch);
-        });
+            it('renders correctly', function () {
+                var data = this.item.model.attributes;
+                this.item.render();
+                expect(this.item.$el).toContainHtml(data.content.display_name);
+                expect(this.item.$el).toContain('a[href="/courses/' + data.course + '/info"]');
+                expect(this.item.$el).toContain('img[src="' + data.image_url + '"]');
+                expect(this.item.$el.find('.course-name')).toContainHtml(data.org);
+                expect(this.item.$el.find('.course-name')).toContainHtml(data.content.number);
+                expect(this.item.$el.find('.course-name')).toContainHtml(data.content.display_name);
+                expect(this.item.$el.find('.course-date')).toContainHtml('Jan 01, 1970');
+            });
 
-        it('trims input string', function () {
-            var term = '  search string  ';
-            $('.discovery-input').val(term);
-            $('form').trigger('submit');
-            expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
         });
 
-        it('handles calls to doSearch', function () {
-            var term = '  search string  ';
-            $('.discovery-input').val(term);
-            this.form.doSearch(term);
-            expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
-            expect($('.discovery-input').val()).toEqual(term);
-            expect($('#discovery-message')).toBeEmpty();
-        });
 
-        it('clears search', function () {
-            $('.discovery-input').val('somethig');
-            this.form.clearSearch();
-            expect($('.discovery-input').val()).toEqual('');
-        });
+        describe('DiscoveryForm', function () {
 
-        it('shows/hides loading indicator', function () {
-            this.form.showLoadingIndicator();
-            expect($('#loading-indicator')).not.toHaveClass('hidden');
-            this.form.hideLoadingIndicator();
-            expect($('#loading-indicator')).toHaveClass('hidden');
-        });
+            beforeEach(function () {
+                loadFixtures('js/fixtures/discovery.html');
+                this.form = new DiscoveryForm();
+                this.onSearch = jasmine.createSpy('onSearch');
+                this.form.on('search', this.onSearch);
+            });
 
-        it('shows messages', function () {
-            this.form.showNotFoundMessage();
-            expect($('#discovery-message')).not.toBeEmpty();
-            this.form.showErrorMessage();
-            expect($('#discovery-message')).not.toBeEmpty();
-        });
+            it('trims input string', function () {
+                var term = '  search string  ';
+                $('.discovery-input').val(term);
+                $('form').trigger('submit');
+                expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
+            });
 
-    });
+            it('handles calls to doSearch', function () {
+                var term = '  search string  ';
+                $('.discovery-input').val(term);
+                this.form.doSearch(term);
+                expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
+                expect($('.discovery-input').val()).toEqual(term);
+                expect($('#discovery-message')).toBeEmpty();
+            });
 
-    describe('FilterBarView', function () {
-        beforeEach(function () {
-            loadFixtures('js/fixtures/discovery.html');
-            TemplateHelpers.installTemplates(
-                ['templates/discovery/filter_bar',
-                'templates/discovery/filter']
-            );
-            this.filterBar = new FiltersBarView();
-            this.onClear = jasmine.createSpy('onClear');
-            this.filterBar.on('clear', this.onClear);
-        });
+            it('clears search', function () {
+                $('.discovery-input').val('somethig');
+                this.form.clearSearch();
+                expect($('.discovery-input').val()).toEqual('');
+            });
 
-        it('view searches for sent facet object', function () {
-            expect(this.filterBar.$el.length).toBe(1);
-            this.filterBar.addFilter(FACET_LIST[0]);
-            expect(this.filterBar.$el.find('#clear-all-filters')).toBeVisible();
-        });
+            it('shows/hides loading indicator', function () {
+                this.form.showLoadingIndicator();
+                expect($('#loading-indicator')).not.toHaveClass('hidden');
+                this.form.hideLoadingIndicator();
+                expect($('#loading-indicator')).toHaveClass('hidden');
+            });
 
-        it('view searches for entered search string', function () {
-            spyOn(this.filterBar, 'addFilter').andCallThrough();
-            expect(this.filterBar.$el.length).toBe(1);
-            this.filterBar.changeQueryFilter(SEARCH_FILTER.query);
-            expect(this.filterBar.$el.find('#clear-all-filters')).toBeVisible();
-            expect(this.filterBar.addFilter).toHaveBeenCalledWith(SEARCH_FILTER);
-        });
+            it('shows messages', function () {
+                this.form.showNotFoundMessage();
+                expect($('#discovery-message')).not.toBeEmpty();
+                this.form.showErrorMessage();
+                expect($('#discovery-message')).not.toBeEmpty();
+            });
 
-        it('model cleans view on destruction correctly', function () {
-            this.filterBar.addFilter(SEARCH_FILTER);
-            var model = this.filterBar.collection.findWhere(SEARCH_FILTER);
-            expect(this.filterBar.$el.find('.active-filter').length).toBe(1);
-            model.cleanModelView();
-            expect(this.filterBar.$el.find('.active-filter').length).toBe(0);
         });
 
-        it('view removes all filters and hides bar if clear all', function () {
-            spyOn(this.filterBar, 'clearAll').andCallThrough();
-            this.filterBar.delegateEvents();
-            this.filterBar.addFilter(SEARCH_FILTER);
-            var clearAll = this.filterBar.$el.find('#clear-all-filters');
-            expect(clearAll).toBeVisible();
-            clearAll.trigger('click');
-            expect(this.filterBar.clearAll).toHaveBeenCalled();
-            expect(this.onClear).toHaveBeenCalled();
-        });
+        describe('FilterBarView', function () {
+            beforeEach(function () {
+                loadFixtures('js/fixtures/discovery.html');
+                TemplateHelpers.installTemplates(
+                    ['templates/discovery/filter_bar',
+                    'templates/discovery/filter']
+                );
+                this.filterBar = new FiltersBarView();
+                this.onClear = jasmine.createSpy('onClear');
+                this.filterBar.on('clear', this.onClear);
+            });
 
-        it('view hides bar if all filters removed', function () {
-            spyOn(this.filterBar, 'clearFilter').andCallThrough();
-            this.filterBar.delegateEvents();
-            this.filterBar.addFilter(SEARCH_FILTER);
-            var clearAll = this.filterBar.$el.find('#clear-all-filters');
-            expect(clearAll).toBeVisible();
-            var filter = this.filterBar.$el.find('li .discovery-button');
-            filter.trigger('click');
-            expect(this.filterBar.clearFilter).toHaveBeenCalled();
-            expect(this.onClear).toHaveBeenCalled();
-        });
+            it('view searches for sent facet object', function () {
+                expect(this.filterBar.$el.length).toBe(1);
+                this.filterBar.addFilter(FACET_LIST[0]);
+                expect(this.filterBar.$el.find('#clear-all-filters')).toBeVisible();
+            });
 
-        it('view changes query filter', function () {
-            this.filterBar.addFilter(SEARCH_FILTER);
-            var filter = $(this.filterBar.$el.find('li .discovery-button')[0]);
-            expect(filter.text().trim()).toBe(SEARCH_FILTER.query);
-            // Have to explicitly remove model because events not dispatched
-            var model = this.filterBar.collection.findWhere(SEARCH_FILTER);
-            model.cleanModelView();
-            this.filterBar.changeQueryFilter(SEARCH_FILTER.query + '2');
-            filter = $(this.filterBar.$el.find('li .discovery-button')[0]);
-            expect(filter.text().trim()).toBe(SEARCH_FILTER.query + '2');
-        });
+            it('view searches for entered search string', function () {
+                spyOn(this.filterBar, 'addFilter').andCallThrough();
+                expect(this.filterBar.$el.length).toBe(1);
+                this.filterBar.changeQueryFilter(SEARCH_FILTER.query);
+                expect(this.filterBar.$el.find('#clear-all-filters')).toBeVisible();
+                expect(this.filterBar.addFilter).toHaveBeenCalledWith(SEARCH_FILTER);
+            });
 
-        it('view returns correct search term', function () {
-            this.filterBar.addFilter(SEARCH_FILTER);
-            expect(this.filterBar.getSearchTerm()).toBe(SEARCH_FILTER.query);
-        });
+            it('model cleans view on destruction correctly', function () {
+                this.filterBar.addFilter(SEARCH_FILTER);
+                var model = this.filterBar.collection.findWhere(SEARCH_FILTER);
+                expect(this.filterBar.$el.find('.active-filter').length).toBe(1);
+                model.cleanModelView();
+                expect(this.filterBar.$el.find('.active-filter').length).toBe(0);
+            });
 
-    });
+            it('view removes all filters and hides bar if clear all', function () {
+                spyOn(this.filterBar, 'clearAll').andCallThrough();
+                this.filterBar.delegateEvents();
+                this.filterBar.addFilter(SEARCH_FILTER);
+                var clearAll = this.filterBar.$el.find('#clear-all-filters');
+                expect(clearAll).toBeVisible();
+                clearAll.trigger('click');
+                expect(this.filterBar.clearAll).toHaveBeenCalled();
+                expect(this.onClear).toHaveBeenCalled();
+            });
 
-    describe('SearchFacetView', function () {
-        beforeEach(function () {
-            loadFixtures('js/fixtures/discovery.html');
-            TemplateHelpers.installTemplates([
-                'templates/discovery/search_facet',
-                'templates/discovery/search_facets_section',
-                'templates/discovery/search_facets_list',
-                'templates/discovery/more_less_links'
-            ]);
-            var facetsTypes = {org: 'Organization', modes: 'Course Type'};
-            this.searchFacetView = new SearchFacetView(facetsTypes);
-            this.searchFacetView.renderFacets(JSON_RESPONSE.facets);
-            this.onAddFilter = jasmine.createSpy('onAddFilter');
-            this.searchFacetView.on('addFilter', this.onAddFilter);
-        });
+            it('view hides bar if all filters removed', function () {
+                spyOn(this.filterBar, 'clearFilter').andCallThrough();
+                this.filterBar.delegateEvents();
+                this.filterBar.addFilter(SEARCH_FILTER);
+                var clearAll = this.filterBar.$el.find('#clear-all-filters');
+                expect(clearAll).toBeVisible();
+                var filter = this.filterBar.$el.find('li .discovery-button');
+                filter.trigger('click');
+                expect(this.filterBar.clearFilter).toHaveBeenCalled();
+                expect(this.onClear).toHaveBeenCalled();
+            });
 
-        it('view expands more content on show more click', function () {
-            var $showMore = this.searchFacetView.$el.find('.show-more');
-            var $showLess = this.searchFacetView.$el.find('.show-less');
-            var $ul = $showMore.parent('div').siblings('ul');
-            expect($showMore).not.toHaveClass('hidden');
-            expect($showLess).toHaveClass('hidden');
-            expect($ul).toHaveClass('collapse');
-            $showMore.trigger('click');
-            expect($showMore).toHaveClass('hidden');
-            expect($showLess).not.toHaveClass('hidden');
-            expect($ul).not.toHaveClass('collapse');
-        });
+            it('view changes query filter', function () {
+                this.filterBar.addFilter(SEARCH_FILTER);
+                var filter = $(this.filterBar.$el.find('li .discovery-button')[0]);
+                expect(filter.text().trim()).toBe(SEARCH_FILTER.query);
+                // Have to explicitly remove model because events not dispatched
+                var model = this.filterBar.collection.findWhere(SEARCH_FILTER);
+                model.cleanModelView();
+                this.filterBar.changeQueryFilter(SEARCH_FILTER.query + '2');
+                filter = $(this.filterBar.$el.find('li .discovery-button')[0]);
+                expect(filter.text().trim()).toBe(SEARCH_FILTER.query + '2');
+            });
 
-        it('view collapses content on show less click', function () {
-            var $showMore = this.searchFacetView.$el.find('.show-more');
-            var $showLess = this.searchFacetView.$el.find('.show-less');
-            var $ul = $showMore.parent('div').siblings('ul');
-            $showMore.trigger('click');
-            expect($showMore).toHaveClass('hidden');
-            expect($showLess).not.toHaveClass('hidden');
-            expect($ul).not.toHaveClass('collapse');
-            $showLess.trigger('click');
-            expect($showMore).not.toHaveClass('hidden');
-            expect($showLess).toHaveClass('hidden');
-            expect($ul).toHaveClass('collapse');
-        });
+            it('view returns correct search term', function () {
+                this.filterBar.addFilter(SEARCH_FILTER);
+                expect(this.filterBar.getSearchTerm()).toBe(SEARCH_FILTER.query);
+            });
 
-        it('view triggers addFilter event if facet is clicked', function () {
-            this.searchFacetView.delegateEvents();
-            var $facetLink = this.searchFacetView.$el.find('li [data-value="edX1"]');
-            var $facet = $facetLink.parent('li');
-            $facet.trigger('click');
-            expect(this.onAddFilter).toHaveBeenCalledWith(
-                {
-                    type: $facet.data('facet'),
-                    query: $facetLink.data('value'),
-                    name : $facetLink.data('text')
-                }
-            );
         });
 
-        it('re-render facets on second click', function () {
-            // First search
-            this.searchFacetView.delegateEvents();
-            this.searchFacetView.renderFacets(JSON_RESPONSE.facets);
-            expect(this.searchFacetView.facetViews.length).toBe(2);
-            // Setup spy
-            var customView = this.searchFacetView.facetViews[0];
-            spyOn(customView, 'remove').andCallThrough();
-            // Second search
-            this.searchFacetView.renderFacets(JSON_RESPONSE.facets);
-            expect(this.searchFacetView.facetViews.length).toBe(2);
-            expect(customView.remove).toHaveBeenCalled();
-        });
+        describe('SearchFacetView', function () {
+            beforeEach(function () {
+                loadFixtures('js/fixtures/discovery.html');
+                TemplateHelpers.installTemplates([
+                    'templates/discovery/search_facet',
+                    'templates/discovery/search_facets_section',
+                    'templates/discovery/search_facets_list',
+                    'templates/discovery/more_less_links'
+                ]);
+                var facetsTypes = {org: 'Organization', modes: 'Course Type'};
+                this.searchFacetView = new SearchFacetView(facetsTypes);
+                this.searchFacetView.renderFacets(JSON_RESPONSE.facets);
+                this.onAddFilter = jasmine.createSpy('onAddFilter');
+                this.searchFacetView.on('addFilter', this.onAddFilter);
+            });
 
-    });
+            it('view expands more content on show more click', function () {
+                var $showMore = this.searchFacetView.$el.find('.show-more');
+                var $showLess = this.searchFacetView.$el.find('.show-less');
+                var $ul = $showMore.parent('div').siblings('ul');
+                expect($showMore).not.toHaveClass('hidden');
+                expect($showLess).toHaveClass('hidden');
+                expect($ul).toHaveClass('collapse');
+                $showMore.trigger('click');
+                expect($showMore).toHaveClass('hidden');
+                expect($showLess).not.toHaveClass('hidden');
+                expect($ul).not.toHaveClass('collapse');
+            });
 
-    describe('ResultListView', function () {
+            it('view collapses content on show less click', function () {
+                var $showMore = this.searchFacetView.$el.find('.show-more');
+                var $showLess = this.searchFacetView.$el.find('.show-less');
+                var $ul = $showMore.parent('div').siblings('ul');
+                $showMore.trigger('click');
+                expect($showMore).toHaveClass('hidden');
+                expect($showLess).not.toHaveClass('hidden');
+                expect($ul).not.toHaveClass('collapse');
+                $showLess.trigger('click');
+                expect($showMore).not.toHaveClass('hidden');
+                expect($showLess).toHaveClass('hidden');
+                expect($ul).toHaveClass('collapse');
+            });
 
-        beforeEach(function () {
-            jasmine.Clock.useMock();
-            loadFixtures('js/fixtures/discovery.html');
-            TemplateHelpers.installTemplate('templates/discovery/result_item');
-            var collection = new Collection([JSON_RESPONSE.results[0].data]);
-            collection.latestModelsCount = 1;
-            this.view = new ResultListView({ collection: collection });
-        });
+            it('view triggers addFilter event if facet is clicked', function () {
+                this.searchFacetView.delegateEvents();
+                var $facetLink = this.searchFacetView.$el.find('li [data-value="edX1"]');
+                var $facet = $facetLink.parent('li');
+                $facet.trigger('click');
+                expect(this.onAddFilter).toHaveBeenCalledWith(
+                    {
+                        type: $facet.data('facet'),
+                        query: $facetLink.data('value'),
+                        name : $facetLink.data('text')
+                    }
+                );
+            });
 
-        it('renders search results', function () {
-            this.view.render();
-            expect($('.courses-listing article').length).toEqual(1);
-            expect($('.courses-listing .course-title')).toContainHtml('edX Demonstration Course');
-            this.view.renderNext();
-            expect($('.courses-listing article').length).toEqual(2);
-        });
+            it('re-render facets on second click', function () {
+                // First search
+                this.searchFacetView.delegateEvents();
+                this.searchFacetView.renderFacets(JSON_RESPONSE.facets);
+                expect(this.searchFacetView.facetViews.length).toBe(2);
+                // Setup spy
+                var customView = this.searchFacetView.facetViews[0];
+                spyOn(customView, 'remove').andCallThrough();
+                // Second search
+                this.searchFacetView.renderFacets(JSON_RESPONSE.facets);
+                expect(this.searchFacetView.facetViews.length).toBe(2);
+                expect(customView.remove).toHaveBeenCalled();
+            });
 
-        it('scrolling triggers an event for next page', function () {
-            this.onNext = jasmine.createSpy('onNext');
-            this.view.on('next', this.onNext);
-            spyOn(this.view.collection, 'hasNextPage').andCallFake(function () {
-                return true;
-            });
-            this.view.render();
-            window.scroll(0, $(document).height());
-            $(window).trigger('scroll');
-            jasmine.Clock.tick(500);
-            expect(this.onNext).toHaveBeenCalled();
-
-            // should not be triggered again (while it is loading)
-            $(window).trigger('scroll');
-            jasmine.Clock.tick(500);
-            expect(this.onNext.calls.length).toEqual(1);
         });
 
-    });
+        describe('ResultListView', function () {
 
+            beforeEach(function () {
+                jasmine.Clock.useMock();
+                loadFixtures('js/fixtures/discovery.html');
+                TemplateHelpers.installTemplate('templates/discovery/result_item');
+                var collection = new Collection([JSON_RESPONSE.results[0].data]);
+                collection.latestModelsCount = 1;
+                this.view = new ResultListView({ collection: collection });
+            });
 
-    describe('Discovery App', function () {
-
-        beforeEach(function () {
-            loadFixtures('js/fixtures/discovery.html');
-            TemplateHelpers.installTemplates([
-                'templates/discovery/result_item',
-                'templates/discovery/filter',
-                'templates/discovery/filter_bar',
-                'templates/discovery/search_facet',
-                'templates/discovery/search_facets_section',
-                'templates/discovery/search_facets_list',
-                'templates/discovery/more_less_links'
-            ]);
-
-            this.app = new App(
-                Collection,
-                DiscoveryForm,
-                ResultListView,
-                FiltersBarView,
-                SearchFacetView
-            );
-        });
+            it('renders search results', function () {
+                this.view.render();
+                expect($('.courses-listing article').length).toEqual(1);
+                expect($('.courses-listing .course-title')).toContainHtml('edX Demonstration Course');
+                this.view.renderNext();
+                expect($('.courses-listing article').length).toEqual(2);
+            });
 
-        it('performs search', function () {
-            var requests = AjaxHelpers.requests(this);
-            $('.discovery-input').val('test');
-            $('.discovery-submit').trigger('click');
-            AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
-            expect($('.courses-listing article').length).toEqual(1);
-            expect($('.courses-listing .course-title')).toContainHtml('edX Demonstration Course');
-            expect($('.active-filter').length).toBe(1);
-        });
+            it('scrolling triggers an event for next page', function () {
+                this.onNext = jasmine.createSpy('onNext');
+                this.view.on('next', this.onNext);
+                spyOn(this.view.collection, 'hasNextPage').andCallFake(function () {
+                    return true;
+                });
+                this.view.render();
+                window.scroll(0, $(document).height());
+                $(window).trigger('scroll');
+                jasmine.Clock.tick(500);
+                expect(this.onNext).toHaveBeenCalled();
+
+                // should not be triggered again (while it is loading)
+                $(window).trigger('scroll');
+                jasmine.Clock.tick(500);
+                expect(this.onNext.calls.length).toEqual(1);
+            });
 
-        it('loads more', function () {
-            var requests = AjaxHelpers.requests(this);
-            jasmine.Clock.useMock();
-            $('.discovery-input').val('test');
-            $('.discovery-submit').trigger('click');
-            AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
-            expect($('.courses-listing article').length).toEqual(1);
-            expect($('.courses-listing .course-title')).toContainHtml('edX Demonstration Course');
-            window.scroll(0, $(document).height());
-            $(window).trigger('scroll');
-            jasmine.Clock.tick(500);
-            AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
-            expect($('.courses-listing article').length).toEqual(2);
         });
 
-        it('displays not found message', function () {
-            var requests = AjaxHelpers.requests(this);
-            $('.discovery-input').val('asdfasdf');
-            $('.discovery-submit').trigger('click');
-            AjaxHelpers.respondWithJson(requests, {});
-            expect($('#discovery-message')).not.toBeEmpty();
-            expect($('.courses-listing')).toBeEmpty();
-        });
 
-        it('displays error message', function () {
-            var requests = AjaxHelpers.requests(this);
-            $('.discovery-input').val('asdfasdf');
-            $('.discovery-submit').trigger('click');
-            AjaxHelpers.respondWithError(requests, 404);
-            expect($('#discovery-message')).not.toBeEmpty();
-            expect($('.courses-listing')).toBeEmpty();
-        });
+        describe('Discovery App', function () {
+
+            beforeEach(function () {
+                loadFixtures('js/fixtures/discovery.html');
+                TemplateHelpers.installTemplates([
+                    'templates/discovery/result_item',
+                    'templates/discovery/filter',
+                    'templates/discovery/filter_bar',
+                    'templates/discovery/search_facet',
+                    'templates/discovery/search_facets_section',
+                    'templates/discovery/search_facets_list',
+                    'templates/discovery/more_less_links'
+                ]);
+
+                DiscoveryFactory(
+                    {
+                        org: {
+                            name: 'Organization',
+                            terms: {
+                                edX1: "edX_1"
+                            }
+                        },
+                        modes: {
+                            name: 'Course Type',
+                            terms: {
+                                honor: 'Honor',
+                                verified: 'Verified'
+                            }
+                        },
+                        language: {
+                            en: 'English',
+                            hr: 'Croatian'
+                        }
+                    }
+                );
+            });
 
-        it('check filters and bar removed on clear all', function () {
-            var requests = AjaxHelpers.requests(this);
-            $('.discovery-input').val('test');
-            $('.discovery-submit').trigger('click');
-            AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
-            expect($('.active-filter').length).toBe(1);
-            expect($('#filter-bar')).not.toHaveClass('hidden');
-            $('#clear-all-filters').trigger('click');
-            expect($('.active-filter').length).toBe(0);
-            expect($('#filter-bar')).toHaveClass('hidden');
-        });
+            it('performs search', function () {
+                var requests = AjaxHelpers.requests(this);
+                $('.discovery-input').val('test');
+                $('.discovery-submit').trigger('click');
+                AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
+                expect($('.courses-listing article').length).toEqual(1);
+                expect($('.courses-listing .course-title')).toContainHtml('edX Demonstration Course');
+                expect($('.active-filter').length).toBe(1);
+            });
 
-        it('check filters and bar removed on last filter cleared', function () {
-            var requests = AjaxHelpers.requests(this);
-            $('.discovery-input').val('test');
-            $('.discovery-submit').trigger('click');
-            AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
-            expect($('.active-filter').length).toBe(1);
-            var $filter = $('.active-filter');
-            $filter.find('.discovery-button').trigger('click');
-            expect($('.active-filter').length).toBe(0);
-        });
+            it('loads more', function () {
+                var requests = AjaxHelpers.requests(this);
+                jasmine.Clock.useMock();
+                $('.discovery-input').val('test');
+                $('.discovery-submit').trigger('click');
+                AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
+                expect($('.courses-listing article').length).toEqual(1);
+                expect($('.courses-listing .course-title')).toContainHtml('edX Demonstration Course');
+                window.scroll(0, $(document).height());
+                $(window).trigger('scroll');
+                jasmine.Clock.tick(500);
+                AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
+                expect($('.courses-listing article').length).toEqual(2);
+            });
 
-        it('filter results by named facet', function () {
-            var requests = AjaxHelpers.requests(this);
-            $('.discovery-input').val('test');
-            $('.discovery-submit').trigger('click');
-            AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
-            expect($('.active-filter').length).toBe(1);
-            var $facetLink = $('.search-facets li [data-value="edX1"]');
-            var $facet = $facetLink.parent('li');
-            $facet.trigger('click');
-            expect($('.active-filter').length).toBe(2);
-            expect($('.active-filter [data-value="edX1"]').length).toBe(1);
-            expect($('.active-filter [data-value="edX1"]').text().trim()).toBe("edX_1");
-        });
+            it('displays not found message', function () {
+                var requests = AjaxHelpers.requests(this);
+                $('.discovery-input').val('asdfasdf');
+                $('.discovery-submit').trigger('click');
+                AjaxHelpers.respondWithJson(requests, {});
+                expect($('#discovery-message')).not.toBeEmpty();
+                expect($('.courses-listing')).toBeEmpty();
+            });
 
-    });
+            it('displays error message', function () {
+                var requests = AjaxHelpers.requests(this);
+                $('.discovery-input').val('asdfasdf');
+                $('.discovery-submit').trigger('click');
+                AjaxHelpers.respondWithError(requests, 404);
+                expect($('#discovery-message')).not.toBeEmpty();
+                expect($('.courses-listing')).toBeEmpty();
+            });
 
+            it('check filters and bar removed on clear all', function () {
+                var requests = AjaxHelpers.requests(this);
+                $('.discovery-input').val('test');
+                $('.discovery-submit').trigger('click');
+                AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
+                expect($('.active-filter').length).toBe(1);
+                expect($('#filter-bar')).not.toHaveClass('hidden');
+                $('#clear-all-filters').trigger('click');
+                expect($('.active-filter').length).toBe(0);
+                expect($('#filter-bar')).toHaveClass('hidden');
+            });
 
+            it('check filters and bar removed on last filter cleared', function () {
+                var requests = AjaxHelpers.requests(this);
+                $('.discovery-input').val('test');
+                $('.discovery-submit').trigger('click');
+                AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
+                expect($('.active-filter').length).toBe(1);
+                var $filter = $('.active-filter');
+                $filter.find('.discovery-button').trigger('click');
+                expect($('.active-filter').length).toBe(0);
+            });
 
+            it('filter results by named facet', function () {
+                var requests = AjaxHelpers.requests(this);
+                $('.discovery-input').val('test');
+                $('.discovery-submit').trigger('click');
+                AjaxHelpers.respondWithJson(requests, JSON_RESPONSE);
+                expect($('.active-filter').length).toBe(1);
+                var $facetLink = $('.search-facets li [data-value="edX1"]');
+                var $facet = $facetLink.parent('li');
+                $facet.trigger('click');
+                expect($('.active-filter').length).toBe(2);
+                expect($('.active-filter [data-value="edX1"]').length).toBe(1);
+                expect($('.active-filter [data-value="edX1"]').text().trim()).toBe("edX_1");
+            });
+        });
+    });
 });
diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js
index 4ce54d5c53f..2532e53f9e8 100644
--- a/lms/static/js/spec/main.js
+++ b/lms/static/js/spec/main.js
@@ -84,9 +84,7 @@
             'js/ccx/schedule': 'js/ccx/schedule',
 
             // edxnotes
-            'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min',
-
-            'course_discovery_meanings': 'js/spec/discovery/course_discovery_meanings'
+            'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min'
         },
         shim: {
             'gettext': {
diff --git a/lms/static/js/spec/search/search_spec.js b/lms/static/js/spec/search/search_spec.js
index 411c7ba405e..6dce51fe99f 100644
--- a/lms/static/js/spec/search/search_spec.js
+++ b/lms/static/js/spec/search/search_spec.js
@@ -13,8 +13,8 @@ define([
     'js/search/dashboard/views/search_form',
     'js/search/course/views/search_results_view',
     'js/search/dashboard/views/search_results_view',
-    'js/search/course/search_app',
-    'js/search/dashboard/search_app'
+    'js/search/course/course_search_factory',
+    'js/search/dashboard/dashboard_search_factory'
 ], function(
     $,
     Sinon,
@@ -30,8 +30,8 @@ define([
     DashSearchForm,
     CourseSearchResultsView,
     DashSearchResultsView,
-    CourseSearchApp,
-    DashSearchApp
+    CourseSearchFactory,
+    DashboardSearchFactory
 ) {
     'use strict';
 
@@ -681,13 +681,7 @@ define([
 
                 this.server = Sinon.fakeServer.create();
                 var courseId = 'a/b/c';
-                this.app = new CourseSearchApp(
-                    courseId,
-                    SearchRouter,
-                    CourseSearchForm,
-                    SearchCollection,
-                    CourseSearchResultsView
-                );
+                CourseSearchFactory(courseId);
                 spyOn(Backbone.history, 'navigate');
                 this.$contentElement = $('#course-content');
                 this.$searchResults = $('#courseware-search-results');
@@ -718,12 +712,7 @@ define([
                 loadTemplates.call(this);
 
                 this.server = Sinon.fakeServer.create();
-                this.app = new DashSearchApp(
-                    SearchRouter,
-                    DashSearchForm,
-                    SearchCollection,
-                    DashSearchResultsView
-                );
+                DashboardSearchFactory();
 
                 spyOn(Backbone.history, 'navigate');
                 this.$contentElement = $('#my-courses');
diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js
index 75b7be52be9..f02c4e070ca 100644
--- a/lms/static/lms/js/build.js
+++ b/lms/static/lms/js/build.js
@@ -18,7 +18,10 @@
          * done.
          */
         modules: getModulesList([
+            'js/discovery/discovery_factory',
             'js/groups/views/cohorts_dashboard_factory',
+            'js/search/course/course_search_factory',
+            'js/search/dashboard/dashboard_search_factory',
             'js/student_account/views/account_settings_factory',
             'js/student_account/views/finish_auth_factory',
             'js/student_profile/views/learner_profile_factory',
@@ -57,7 +60,7 @@
             'underscore': 'empty:',
             'logger': 'empty:',
             'utility': 'empty:',
-            'URI': 'empty:'
+            'URI': 'empty:',
         },
 
         /**
diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html
index 671fe360d92..171a4edd646 100644
--- a/lms/templates/courseware/courses.html
+++ b/lms/templates/courseware/courses.html
@@ -2,33 +2,27 @@
   import json
   from django.utils.translation import ugettext as _
   from microsite_configuration import microsite
+  from openedx.core.lib.json_utils import EscapedEdxJSONEncoder
 %>
 <%inherit file="../main.html" />
 
 <%namespace name='static' file='../static_content.html'/>
 
+% if settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
 <%block name="header_extras">
-  % if settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
   % for template_name in ["result_item", "filter_bar", "filter", "search_facets_list", "search_facets_section", "search_facet", "more_less_links"]:
   <script type="text/template" id="${template_name}-tpl">
       <%static:include path="discovery/${template_name}.underscore" />
   </script>
   % endfor
-  <script type="text/javascript">;(function (define) {{
-      define('course_discovery_meanings', function() {{
-          'use strict';
-          return ${json.dumps(course_discovery_meanings)};
-        }});
-      }})(define || RequireJS.define);
-  </script>
-  % endif
-</%block>
-
-<%block name="js_extra">
-  % if settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
-  <%static:js group='discovery'/>
-  % endif
+  <%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory">
+    DiscoveryFactory(
+      ${json.dumps(course_discovery_meanings, cls=EscapedEdxJSONEncoder)},
+      getParameterByName('search_query')
+    );
+  </%static:require_module>
 </%block>
+% endif
 
 <%block name="pagetitle">${_("Courses")}</%block>
 <%
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index 7e3e3862c0e..b4c8e5d1653 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -66,7 +66,10 @@ ${page_title_breadcrumbs(course_name())}
   <%static:js group='courseware'/>
   <%static:js group='discussion'/>
   % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
-    <%static:js group='courseware_search'/>
+    <%static:require_module module_name="js/search/course/course_search_factory" class_name="CourseSearchFactory">
+        var courseId = $('#courseware-search-results').data('courseId');
+        CourseSearchFactory(courseId);
+    </%static:require_module>
   % endif
 
   <%include file="../discussion/_js_body_dependencies.html" />
diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html
index 5d1ffc4c887..07181b65d57 100644
--- a/lms/templates/dashboard.html
+++ b/lms/templates/dashboard.html
@@ -45,7 +45,9 @@ from django.core.urlresolvers import reverse
     });
   </script>
   % if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
-    <%static:js group='dashboard_search'/>
+    <%static:require_module module_name="js/search/dashboard/dashboard_search_factory" class_name="DashboardSearchFactory">
+        DashboardSearchFactory();
+    </%static:require_module>
   % endif
 </%block>
 
-- 
GitLab