Merge pull request #7522 from edx/feature/dashboard-search
Feature/dashboard search
This commit is contained in:
12
lms/static/js/fixtures/search/course_search_form.html
Normal file
12
lms/static/js/fixtures/search/course_search_form.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<div id="courseware-search-bar" class="search-bar" role="search" aria-label="Course">
|
||||
<form>
|
||||
<label for="course-search-input" class="sr">Course Search</label>
|
||||
<input id="course-search-input" type="text" class="search-field"/>
|
||||
<button type="submit" class="search-button">
|
||||
search <i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="cancel-button" aria-label="Clear search">
|
||||
<i class="icon fa fa-remove" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
14
lms/static/js/fixtures/search/dashboard_search_form.html
Normal file
14
lms/static/js/fixtures/search/dashboard_search_form.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<div id="dashboard-search-bar" class="search-bar" role="search" aria-label="Dashboard">
|
||||
<form>
|
||||
<label for="dashboard-search-input">Search Your Courses</label>
|
||||
<div>
|
||||
<input id="dashboard-search-input" type="text" class="search-field"/>
|
||||
<button type="submit" class="search-button" aria-label="Search">
|
||||
<i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="cancel-button" aria-label="Clear search">
|
||||
<i class="icon fa fa-remove" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
<div id="courseware-search-bar" class="search-container">
|
||||
<form role="search-form">
|
||||
<input type="text" class="search-field"/>
|
||||
<button type="submit" class="search-button" aria-label="Search">search <i class="icon-search"></i></button>
|
||||
<button type="button" class="cancel-button" aria-label="Cancel"><i class="icon-remove"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
define([
|
||||
'backbone',
|
||||
'js/search/models/search_result'
|
||||
'js/search/base/models/search_result'
|
||||
], function (Backbone, SearchResult) {
|
||||
'use strict';
|
||||
|
||||
@@ -11,6 +11,7 @@ define([
|
||||
model: SearchResult,
|
||||
pageSize: 20,
|
||||
totalCount: 0,
|
||||
latestModelsCount: 0,
|
||||
accessDeniedCount: 0,
|
||||
searchTerm: '',
|
||||
page: 0,
|
||||
@@ -20,17 +21,15 @@ define([
|
||||
initialize: function (models, options) {
|
||||
// call super constructor
|
||||
Backbone.Collection.prototype.initialize.apply(this, arguments);
|
||||
if (options && options.course_id) {
|
||||
this.url += options.course_id;
|
||||
if (options && options.courseId) {
|
||||
this.url += options.courseId;
|
||||
}
|
||||
},
|
||||
|
||||
performSearch: function (searchTerm) {
|
||||
this.fetchXhr && this.fetchXhr.abort();
|
||||
this.searchTerm = searchTerm || '';
|
||||
this.totalCount = 0;
|
||||
this.accessDeniedCount = 0;
|
||||
this.page = 0;
|
||||
this.resetState();
|
||||
this.fetchXhr = this.fetch({
|
||||
data: {
|
||||
search_string: searchTerm,
|
||||
@@ -71,20 +70,34 @@ define([
|
||||
|
||||
cancelSearch: function () {
|
||||
this.fetchXhr && this.fetchXhr.abort();
|
||||
this.page = 0;
|
||||
this.totalCount = 0;
|
||||
this.accessDeniedCount = 0;
|
||||
this.resetState();
|
||||
},
|
||||
|
||||
parse: function(response) {
|
||||
this.latestModelsCount = response.results.length;
|
||||
this.totalCount = response.total;
|
||||
this.accessDeniedCount += response.access_denied_count;
|
||||
this.totalCount -= this.accessDeniedCount;
|
||||
return _.map(response.results, function(result){ return result.data; });
|
||||
return _.map(response.results, function (result) {
|
||||
return result.data;
|
||||
});
|
||||
},
|
||||
|
||||
resetState: function () {
|
||||
this.page = 0;
|
||||
this.totalCount = 0;
|
||||
this.latestModelsCount = 0;
|
||||
this.accessDeniedCount = 0;
|
||||
// empty the entire collection
|
||||
this.reset();
|
||||
},
|
||||
|
||||
hasNextPage: function () {
|
||||
return this.totalCount - ((this.page + 1) * this.pageSize) > 0;
|
||||
},
|
||||
|
||||
latestModels: function () {
|
||||
return this.last(this.latestModelsCount);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -4,9 +4,12 @@ define(['backbone'], function (Backbone) {
|
||||
'use strict';
|
||||
|
||||
return Backbone.Router.extend({
|
||||
routes: {
|
||||
'search/:query': 'search'
|
||||
}
|
||||
routes: {
|
||||
'search/:query': 'search'
|
||||
},
|
||||
search: function(query) {
|
||||
this.trigger('search', query);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -5,7 +5,7 @@ define(['jquery', 'backbone'], function ($, Backbone) {
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
el: '#courseware-search-bar',
|
||||
el: '',
|
||||
events: {
|
||||
'submit form': 'submitForm',
|
||||
'click .cancel-button': 'clearSearch',
|
||||
@@ -40,9 +40,13 @@ define(['jquery', 'backbone'], function ($, Backbone) {
|
||||
}
|
||||
},
|
||||
|
||||
clearSearch: function () {
|
||||
resetSearchForm: function () {
|
||||
this.$searchField.val('');
|
||||
this.setInitialStyle();
|
||||
},
|
||||
|
||||
clearSearch: function () {
|
||||
this.resetSearchForm();
|
||||
this.trigger('clear');
|
||||
},
|
||||
|
||||
@@ -7,11 +7,12 @@ define([
|
||||
'gettext',
|
||||
'logger'
|
||||
], function ($, _, Backbone, gettext, Logger) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
tagName: 'li',
|
||||
templateId: '',
|
||||
className: 'search-results-item',
|
||||
attributes: {
|
||||
'role': 'region',
|
||||
@@ -19,16 +20,22 @@ define([
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .search-results-item a': 'logSearchItem',
|
||||
'click': 'logSearchItem',
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
var template_name = (this.model.attributes.content_type === "Sequence") ? '#search_item_seq-tpl' : '#search_item-tpl';
|
||||
this.tpl = _.template($(template_name).html());
|
||||
this.tpl = _.template($(this.templateId).html());
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.tpl(this.model.attributes));
|
||||
var data = _.clone(this.model.attributes);
|
||||
// Drop the preview text and result type if the search term is found
|
||||
// in the title/location in the course hierarchy
|
||||
if (this.model.get('content_type') === 'Sequence') {
|
||||
data.excerpt = '';
|
||||
data.content_type = '';
|
||||
}
|
||||
this.$el.html(this.tpl(data));
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -44,24 +51,23 @@ define([
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
var target = this.model.id;
|
||||
var link = $(event.target).attr('href');
|
||||
var link = this.model.get('url');
|
||||
var collection = this.model.collection;
|
||||
var page = collection.page;
|
||||
var pageSize = collection.pageSize;
|
||||
var searchTerm = collection.searchTerm;
|
||||
var index = collection.indexOf(this.model);
|
||||
Logger.log("edx.course.search.result_selected",
|
||||
{
|
||||
"search_term": searchTerm,
|
||||
"result_position": (page * pageSize + index),
|
||||
"result_link": target
|
||||
}).always(function() {
|
||||
self.redirect(link);
|
||||
});
|
||||
Logger.log('edx.course.search.result_selected', {
|
||||
'search_term': searchTerm,
|
||||
'result_position': (page * pageSize + index),
|
||||
'result_link': target
|
||||
}).always(function() {
|
||||
self.redirect(link);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
|
||||
@@ -5,39 +5,39 @@ define([
|
||||
'underscore',
|
||||
'backbone',
|
||||
'gettext',
|
||||
'js/search/views/search_item_view'
|
||||
], function ($, _, Backbone, gettext, SearchItemView) {
|
||||
], function ($, _, Backbone, gettext) {
|
||||
|
||||
'use strict';
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
el: '#courseware-search-results',
|
||||
events: {
|
||||
'click .search-load-next': 'loadNext'
|
||||
},
|
||||
spinner: '.icon',
|
||||
// these should be defined by subclasses
|
||||
el: '',
|
||||
contentElement: '',
|
||||
resultsTemplateId: '',
|
||||
loadingTemplateId: '',
|
||||
errorTemplateId: '',
|
||||
events: {},
|
||||
spinner: '.search-load-next .icon',
|
||||
SearchItemView: function () {},
|
||||
|
||||
initialize: function () {
|
||||
this.courseName = this.$el.attr('data-course-name');
|
||||
this.$courseContent = $('#course-content');
|
||||
this.listTemplate = _.template($('#search_list-tpl').html());
|
||||
this.loadingTemplate = _.template($('#search_loading-tpl').html());
|
||||
this.errorTemplate = _.template($('#search_error-tpl').html());
|
||||
this.collection.on('search', this.render, this);
|
||||
this.collection.on('next', this.renderNext, this);
|
||||
this.collection.on('error', this.showErrorMessage, this);
|
||||
this.$contentElement = $(this.contentElement);
|
||||
this.resultsTemplate = _.template($(this.resultsTemplateId).html());
|
||||
this.loadingTemplate = _.template($(this.loadingTemplateId).html());
|
||||
this.errorTemplate = _.template($(this.errorTemplateId).html());
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.listTemplate({
|
||||
this.$el.html(this.resultsTemplate({
|
||||
totalCount: this.collection.totalCount,
|
||||
totalCountMsg: this.totalCountMsg(),
|
||||
pageSize: this.collection.pageSize,
|
||||
hasMoreResults: this.collection.hasNextPage()
|
||||
}));
|
||||
this.renderItems();
|
||||
this.$courseContent.hide();
|
||||
this.$el.find(this.spinner).hide();
|
||||
this.$contentElement.hide();
|
||||
this.$el.show();
|
||||
return this;
|
||||
},
|
||||
@@ -53,11 +53,12 @@ define([
|
||||
},
|
||||
|
||||
renderItems: function () {
|
||||
var items = this.collection.map(function (result) {
|
||||
var item = new SearchItemView({ model: result });
|
||||
var latest = this.collection.latestModels();
|
||||
var items = latest.map(function (result) {
|
||||
var item = new this.SearchItemView({ model: result });
|
||||
return item.render().el;
|
||||
});
|
||||
this.$el.find('.search-results').html(items);
|
||||
}, this);
|
||||
this.$el.find('ol').append(items);
|
||||
},
|
||||
|
||||
totalCountMsg: function () {
|
||||
@@ -67,25 +68,26 @@ define([
|
||||
|
||||
clear: function () {
|
||||
this.$el.hide().empty();
|
||||
this.$courseContent.show();
|
||||
this.$contentElement.show();
|
||||
},
|
||||
|
||||
showLoadingMessage: function () {
|
||||
this.$el.html(this.loadingTemplate());
|
||||
this.$el.show();
|
||||
this.$courseContent.hide();
|
||||
this.$contentElement.hide();
|
||||
},
|
||||
|
||||
showErrorMessage: function () {
|
||||
this.$el.html(this.errorTemplate());
|
||||
this.$el.show();
|
||||
this.$courseContent.hide();
|
||||
this.$contentElement.hide();
|
||||
},
|
||||
|
||||
loadNext: function (event) {
|
||||
event && event.preventDefault();
|
||||
this.$el.find(this.spinner).show();
|
||||
this.trigger('next');
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
22
lms/static/js/search/course/main.js
Normal file
22
lms/static/js/search/course/main.js
Normal file
@@ -0,0 +1,22 @@
|
||||
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();
|
||||
|
||||
});
|
||||
50
lms/static/js/search/course/search_app.js
Normal file
50
lms/static/js/search/course/search_app.js
Normal file
@@ -0,0 +1,50 @@
|
||||
;(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);
|
||||
14
lms/static/js/search/course/views/search_form.js
Normal file
14
lms/static/js/search/course/views/search_form.js
Normal file
@@ -0,0 +1,14 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'js/search/base/views/search_form'
|
||||
], function (SearchForm) {
|
||||
'use strict';
|
||||
|
||||
return SearchForm.extend({
|
||||
el: '#courseware-search-bar'
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
14
lms/static/js/search/course/views/search_item_view.js
Normal file
14
lms/static/js/search/course/views/search_item_view.js
Normal file
@@ -0,0 +1,14 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'js/search/base/views/search_item_view'
|
||||
], function (SearchItemView) {
|
||||
'use strict';
|
||||
|
||||
return SearchItemView.extend({
|
||||
templateId: '#course_search_item-tpl'
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
26
lms/static/js/search/course/views/search_results_view.js
Normal file
26
lms/static/js/search/course/views/search_results_view.js
Normal file
@@ -0,0 +1,26 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'js/search/base/views/search_results_view',
|
||||
'js/search/course/views/search_item_view'
|
||||
], function (SearchResultsView, CourseSearchItemView) {
|
||||
'use strict';
|
||||
|
||||
return SearchResultsView.extend({
|
||||
|
||||
el: '#courseware-search-results',
|
||||
contentElement: '#course-content',
|
||||
resultsTemplateId: '#course_search_results-tpl',
|
||||
loadingTemplateId: '#search_loading-tpl',
|
||||
errorTemplateId: '#search_error-tpl',
|
||||
events: {
|
||||
'click .search-load-next': 'loadNext',
|
||||
},
|
||||
SearchItemView: CourseSearchItemView
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
})(define || RequireJS.define);
|
||||
19
lms/static/js/search/dashboard/main.js
Normal file
19
lms/static/js/search/dashboard/main.js
Normal file
@@ -0,0 +1,19 @@
|
||||
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();
|
||||
|
||||
});
|
||||
54
lms/static/js/search/dashboard/search_app.js
Normal file
54
lms/static/js/search/dashboard/search_app.js
Normal file
@@ -0,0 +1,54 @@
|
||||
;(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);
|
||||
14
lms/static/js/search/dashboard/views/search_form.js
Normal file
14
lms/static/js/search/dashboard/views/search_form.js
Normal file
@@ -0,0 +1,14 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'js/search/base/views/search_form'
|
||||
], function (SearchForm) {
|
||||
'use strict';
|
||||
|
||||
return SearchForm.extend({
|
||||
el: '#dashboard-search-bar'
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
14
lms/static/js/search/dashboard/views/search_item_view.js
Normal file
14
lms/static/js/search/dashboard/views/search_item_view.js
Normal file
@@ -0,0 +1,14 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'js/search/base/views/search_item_view'
|
||||
], function (SearchItemView) {
|
||||
'use strict';
|
||||
|
||||
return SearchItemView.extend({
|
||||
templateId: '#dashboard_search_item-tpl'
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
33
lms/static/js/search/dashboard/views/search_results_view.js
Normal file
33
lms/static/js/search/dashboard/views/search_results_view.js
Normal file
@@ -0,0 +1,33 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'js/search/base/views/search_results_view',
|
||||
'js/search/dashboard/views/search_item_view'
|
||||
], function (SearchResultsView, DashSearchItemView) {
|
||||
'use strict';
|
||||
|
||||
return SearchResultsView.extend({
|
||||
|
||||
el: '#dashboard-search-results',
|
||||
contentElement: '#my-courses, #profile-sidebar',
|
||||
resultsTemplateId: '#dashboard_search_results-tpl',
|
||||
loadingTemplateId: '#search_loading-tpl',
|
||||
errorTemplateId: '#search_error-tpl',
|
||||
events: {
|
||||
'click .search-load-next': 'loadNext',
|
||||
'click .search-back-to-courses': 'backToCourses'
|
||||
},
|
||||
SearchItemView: DashSearchItemView,
|
||||
|
||||
backToCourses: function () {
|
||||
this.clear();
|
||||
this.trigger('reset');
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
})(define || RequireJS.define);
|
||||
@@ -1,12 +0,0 @@
|
||||
RequireJS.require([
|
||||
'jquery',
|
||||
'backbone',
|
||||
'js/search/search_app'
|
||||
], function ($, Backbone, SearchApp) {
|
||||
'use strict';
|
||||
|
||||
var course_id = $('#courseware-search-results').attr('data-course-id');
|
||||
var app = new SearchApp(course_id);
|
||||
Backbone.history.start();
|
||||
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
;(function (define) {
|
||||
|
||||
define([
|
||||
'backbone',
|
||||
'js/search/search_router',
|
||||
'js/search/views/search_form',
|
||||
'js/search/views/search_list_view',
|
||||
'js/search/collections/search_collection'
|
||||
], function(Backbone, SearchRouter, SearchForm, SearchListView, SearchCollection) {
|
||||
'use strict';
|
||||
|
||||
return function (course_id) {
|
||||
|
||||
var self = this;
|
||||
|
||||
this.router = new SearchRouter();
|
||||
this.form = new SearchForm();
|
||||
this.collection = new SearchCollection([], { course_id: course_id });
|
||||
this.results = new SearchListView({ collection: this.collection });
|
||||
|
||||
this.form.on('search', this.results.showLoadingMessage, this.results);
|
||||
this.form.on('search', this.collection.performSearch, this.collection);
|
||||
this.form.on('search', function (term) {
|
||||
self.router.navigate('search/' + term, { replace: true });
|
||||
});
|
||||
this.form.on('clear', this.collection.cancelSearch, this.collection);
|
||||
this.form.on('clear', this.results.clear, this.results);
|
||||
this.form.on('clear', this.router.navigate, this.router);
|
||||
|
||||
this.results.on('next', this.collection.loadNextPage, this.collection);
|
||||
this.router.on('route:search', this.form.doSearch, this.form);
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
})(define || RequireJS.define);
|
||||
@@ -4,136 +4,37 @@ define([
|
||||
'backbone',
|
||||
'logger',
|
||||
'js/common_helpers/template_helpers',
|
||||
'js/search/views/search_form',
|
||||
'js/search/views/search_item_view',
|
||||
'js/search/views/search_list_view',
|
||||
'js/search/models/search_result',
|
||||
'js/search/collections/search_collection',
|
||||
'js/search/search_router',
|
||||
'js/search/search_app'
|
||||
'js/search/base/models/search_result',
|
||||
'js/search/base/collections/search_collection',
|
||||
'js/search/base/routers/search_router',
|
||||
'js/search/course/views/search_item_view',
|
||||
'js/search/dashboard/views/search_item_view',
|
||||
'js/search/course/views/search_form',
|
||||
'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'
|
||||
], function(
|
||||
$,
|
||||
Sinon,
|
||||
Backbone,
|
||||
Logger,
|
||||
TemplateHelpers,
|
||||
SearchForm,
|
||||
SearchItemView,
|
||||
SearchListView,
|
||||
SearchResult,
|
||||
SearchCollection,
|
||||
SearchRouter,
|
||||
SearchApp
|
||||
CourseSearchItemView,
|
||||
DashSearchItemView,
|
||||
CourseSearchForm,
|
||||
DashSearchForm,
|
||||
CourseSearchResultsView,
|
||||
DashSearchResultsView,
|
||||
CourseSearchApp,
|
||||
DashSearchApp
|
||||
) {
|
||||
'use strict';
|
||||
|
||||
describe('SearchForm', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/search_form.html');
|
||||
this.form = new SearchForm();
|
||||
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', function () {
|
||||
var term = ' search string ';
|
||||
$('.search-field').val(term);
|
||||
$('form').trigger('submit');
|
||||
expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
|
||||
});
|
||||
|
||||
it('handles calls to doSearch', function () {
|
||||
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();
|
||||
});
|
||||
|
||||
it('triggers a search event and changes to active state', function () {
|
||||
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();
|
||||
});
|
||||
|
||||
it('clears search when clicking on cancel button', function () {
|
||||
$('.search-field').val('search string');
|
||||
$('.cancel-button').trigger('click');
|
||||
expect($('.search-field')).not.toHaveClass('is-active');
|
||||
expect($('.search-button')).toBeVisible();
|
||||
expect($('.cancel-button')).toBeHidden();
|
||||
expect($('.search-field')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('clears search when search box is empty', function() {
|
||||
$('.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('SearchItemView', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
TemplateHelpers.installTemplate('templates/courseware_search/search_item');
|
||||
TemplateHelpers.installTemplate('templates/courseware_search/search_item_seq');
|
||||
this.model = {
|
||||
attributes: {
|
||||
location: ['section', 'subsection', 'unit'],
|
||||
content_type: 'Video',
|
||||
excerpt: 'A short excerpt.',
|
||||
url: 'path/to/content'
|
||||
}
|
||||
};
|
||||
this.item = new SearchItemView({ model: this.model });
|
||||
});
|
||||
|
||||
it('has useful html attributes', function () {
|
||||
expect(this.item.$el).toHaveAttr('role', 'region');
|
||||
expect(this.item.$el).toHaveAttr('aria-label', 'search result');
|
||||
});
|
||||
|
||||
it('renders correctly', function () {
|
||||
var href = this.model.attributes.url;
|
||||
var breadcrumbs = 'section ▸ subsection ▸ unit';
|
||||
|
||||
this.item.render();
|
||||
expect(this.item.$el).toContainHtml(this.model.attributes.content_type);
|
||||
expect(this.item.$el).toContainHtml(this.model.attributes.excerpt);
|
||||
expect(this.item.$el).toContain('a[href="'+href+'"]');
|
||||
expect(this.item.$el).toContainHtml(breadcrumbs);
|
||||
});
|
||||
|
||||
it('log request on follow item link', function () {
|
||||
this.model.collection = new SearchCollection([this.model], { course_id: 'edx101' });
|
||||
this.item.render();
|
||||
// Mock the redirect call
|
||||
spyOn(this.item, 'redirect').andCallFake( function() {} );
|
||||
spyOn(this.item, 'logSearchItem').andCallThrough();
|
||||
spyOn(Logger, 'log').andReturn($.Deferred().resolve());
|
||||
var link = this.item.$el.find('a');
|
||||
expect(link.length).toBe(1);
|
||||
link.trigger('click');
|
||||
expect(this.item.redirect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('SearchResult', function () {
|
||||
|
||||
@@ -171,9 +72,16 @@ define([
|
||||
this.server.restore();
|
||||
});
|
||||
|
||||
it('appends course_id to url', function () {
|
||||
var collection = new SearchCollection([], { course_id: 'edx101' });
|
||||
expect(collection.url).toEqual('/search/edx101');
|
||||
it('sends a request without a course ID', function () {
|
||||
var collection = new SearchCollection([]);
|
||||
collection.performSearch('search string');
|
||||
expect(this.server.requests[0].url).toEqual('/search/');
|
||||
});
|
||||
|
||||
it('sends a request with course ID', function () {
|
||||
var collection = new SearchCollection([], { courseId: 'edx101' });
|
||||
collection.performSearch('search string');
|
||||
expect(this.server.requests[0].url).toEqual('/search/edx101');
|
||||
});
|
||||
|
||||
it('sends a request and parses the json result', function () {
|
||||
@@ -195,6 +103,7 @@ define([
|
||||
|
||||
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);
|
||||
@@ -216,8 +125,9 @@ define([
|
||||
});
|
||||
|
||||
it('sends correct paging parameters', function () {
|
||||
this.collection.performSearch('search string');
|
||||
var searchString = 'search string';
|
||||
var response = { total: 52, results: [] };
|
||||
this.collection.performSearch(searchString);
|
||||
this.server.respondWith('POST', this.collection.url, [200, {}, JSON.stringify(response)]);
|
||||
this.server.respond();
|
||||
this.collection.loadNextPage();
|
||||
@@ -225,11 +135,9 @@ define([
|
||||
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
|
||||
});
|
||||
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 () {
|
||||
@@ -266,18 +174,23 @@ define([
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -285,167 +198,368 @@ define([
|
||||
});
|
||||
|
||||
|
||||
describe('SearchListView', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures(
|
||||
'<section id="courseware-search-results" data-course-name="Test Course"></section>' +
|
||||
'<section id="course-content"></section>'
|
||||
);
|
||||
|
||||
TemplateHelpers.installTemplates([
|
||||
'templates/courseware_search/search_item',
|
||||
'templates/courseware_search/search_item_seq',
|
||||
'templates/courseware_search/search_list',
|
||||
'templates/courseware_search/search_loading',
|
||||
'templates/courseware_search/search_error'
|
||||
]);
|
||||
|
||||
var MockCollection = Backbone.Collection.extend({
|
||||
hasNextPage: function (){}
|
||||
});
|
||||
this.collection = new MockCollection();
|
||||
|
||||
// spy on these methods before they are bound to events
|
||||
spyOn(SearchListView.prototype, 'render').andCallThrough();
|
||||
spyOn(SearchListView.prototype, 'renderNext').andCallThrough();
|
||||
spyOn(SearchListView.prototype, 'showErrorMessage').andCallThrough();
|
||||
|
||||
this.listView = new SearchListView({ collection: this.collection });
|
||||
});
|
||||
|
||||
it('shows loading message', function () {
|
||||
this.listView.showLoadingMessage();
|
||||
expect($('#course-content')).toBeHidden();
|
||||
expect(this.listView.$el).toBeVisible();
|
||||
expect(this.listView.$el).not.toBeEmpty();
|
||||
});
|
||||
|
||||
it('shows error message', function () {
|
||||
this.listView.showErrorMessage();
|
||||
expect($('#course-content')).toBeHidden();
|
||||
expect(this.listView.$el).toBeVisible();
|
||||
expect(this.listView.$el).not.toBeEmpty();
|
||||
});
|
||||
|
||||
it('returns to content', function () {
|
||||
this.listView.clear();
|
||||
expect($('#course-content')).toBeVisible();
|
||||
expect(this.listView.$el).toBeHidden();
|
||||
expect(this.listView.$el).toBeEmpty();
|
||||
});
|
||||
|
||||
it('handles events', function () {
|
||||
this.collection.trigger('search');
|
||||
this.collection.trigger('next');
|
||||
this.collection.trigger('error');
|
||||
|
||||
expect(this.listView.render).toHaveBeenCalled();
|
||||
expect(this.listView.renderNext).toHaveBeenCalled();
|
||||
expect(this.listView.showErrorMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders a message when there are no results', function () {
|
||||
this.collection.reset();
|
||||
this.listView.render();
|
||||
expect(this.listView.$el).toContainHtml('no results');
|
||||
expect(this.listView.$el.find('ol')).not.toExist();
|
||||
});
|
||||
|
||||
it('renders search results', function () {
|
||||
var searchResults = [{
|
||||
location: ['section', 'subsection', 'unit'],
|
||||
url: '/some/url/to/content',
|
||||
content_type: 'text',
|
||||
excerpt: 'this is a short excerpt'
|
||||
}];
|
||||
this.collection.set(searchResults);
|
||||
this.collection.totalCount = 1;
|
||||
|
||||
this.listView.render();
|
||||
expect(this.listView.$el.find('ol')[0]).toExist();
|
||||
expect(this.listView.$el.find('li').length).toEqual(1);
|
||||
expect(this.listView.$el).toContainHtml('Search Results');
|
||||
expect(this.listView.$el).toContainHtml('this is a short excerpt');
|
||||
|
||||
searchResults[1] = searchResults[0]
|
||||
this.collection.set(searchResults);
|
||||
this.collection.totalCount = 2;
|
||||
this.listView.renderNext();
|
||||
expect(this.listView.$el.find('.search-count')).toContainHtml('2');
|
||||
expect(this.listView.$el.find('li').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('shows a link to load more results', function () {
|
||||
this.collection.totalCount = 123;
|
||||
this.collection.hasNextPage = function () { return true; };
|
||||
this.listView.render();
|
||||
expect(this.listView.$el.find('a.search-load-next')[0]).toExist();
|
||||
|
||||
this.collection.totalCount = 123;
|
||||
this.collection.hasNextPage = function () { return false; };
|
||||
this.listView.render();
|
||||
expect(this.listView.$el.find('a.search-load-next')[0]).not.toExist();
|
||||
});
|
||||
|
||||
it('triggers an event for next page', function () {
|
||||
var onNext = jasmine.createSpy('onNext');
|
||||
this.listView.on('next', onNext);
|
||||
this.collection.totalCount = 123;
|
||||
this.collection.hasNextPage = function () { return true; };
|
||||
this.listView.render();
|
||||
this.listView.$el.find('a.search-load-next').click();
|
||||
expect(onNext).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a spinner when loading more results', function () {
|
||||
this.collection.totalCount = 123;
|
||||
this.collection.hasNextPage = function () { return true; };
|
||||
this.listView.render();
|
||||
this.listView.loadNext();
|
||||
|
||||
// Do we really need to check if a loading indicator exists? - CR
|
||||
|
||||
// jasmine.Clock.useMock(1000);
|
||||
// expect(this.listView.$el.find('a.search-load-next .icon')[0]).toBeVisible();
|
||||
this.listView.renderNext();
|
||||
// jasmine.Clock.useMock(1000);
|
||||
// expect(this.listView.$el.find('a.search-load-next .icon')[0]).toBeHidden();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
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.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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
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;
|
||||
|
||||
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(
|
||||
'<section id="courseware-search-results"></section>' +
|
||||
'<section id="course-content"></section>' +
|
||||
'<section id="dashboard-search-results"></section>' +
|
||||
'<section id="my-courses"></section>'
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 () {
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/search_form.html');
|
||||
appendSetFixtures(
|
||||
'<section id="courseware-search-results" data-course-name="Test Course"></section>' +
|
||||
'<section id="course-content"></section>'
|
||||
);
|
||||
TemplateHelpers.installTemplates([
|
||||
'templates/courseware_search/search_item',
|
||||
'templates/courseware_search/search_item_seq',
|
||||
'templates/courseware_search/search_list',
|
||||
'templates/courseware_search/search_loading',
|
||||
'templates/courseware_search/search_error'
|
||||
]);
|
||||
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.server = Sinon.fakeServer.create();
|
||||
function performsSearch () {
|
||||
$('.search-field').val('search string');
|
||||
$('.search-button').trigger('click');
|
||||
this.server.respondWith([200, {}, JSON.stringify({
|
||||
total: 1337,
|
||||
access_denied_count: 12,
|
||||
@@ -454,82 +568,203 @@ define([
|
||||
location: ['section', 'subsection', 'unit'],
|
||||
url: '/some/url/to/content',
|
||||
content_type: 'text',
|
||||
excerpt: 'this is a short excerpt'
|
||||
excerpt: 'this is a short excerpt',
|
||||
course_name: ''
|
||||
}
|
||||
}]
|
||||
})]);
|
||||
|
||||
Backbone.history.stop();
|
||||
this.app = new SearchApp('a/b/c');
|
||||
|
||||
// start history after the application has finished creating
|
||||
// all of its routers
|
||||
Backbone.history.start();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.server.restore();
|
||||
});
|
||||
|
||||
it ('shows loading message on search', function () {
|
||||
$('.search-field').val('search string');
|
||||
$('.search-button').trigger('click');
|
||||
expect($('#course-content')).toBeHidden();
|
||||
expect($('#courseware-search-results')).toBeVisible();
|
||||
expect($('#courseware-search-results')).not.toBeEmpty();
|
||||
});
|
||||
|
||||
it ('performs search', function () {
|
||||
$('.search-field').val('search string');
|
||||
$('.search-button').trigger('click');
|
||||
this.server.respond();
|
||||
expect($('.search-info')).toExist();
|
||||
expect($('.search-results')).toBeVisible();
|
||||
});
|
||||
expect($('.search-result-list')).toBeVisible();
|
||||
expect(this.$searchResults.find('li').length).toEqual(1);
|
||||
}
|
||||
|
||||
it ('updates navigation history on search', function () {
|
||||
function showsErrorMessage () {
|
||||
$('.search-field').val('search string');
|
||||
$('.search-button').trigger('click');
|
||||
this.server.respondWith([500, {}]);
|
||||
this.server.respond();
|
||||
expect(this.$searchResults).toEqual($('#search_error-tpl'));
|
||||
}
|
||||
|
||||
function updatesNavigationHistory () {
|
||||
$('.search-field').val('edx');
|
||||
$('.search-button').trigger('click');
|
||||
expect(Backbone.history.fragment).toEqual('search/edx');
|
||||
});
|
||||
expect(Backbone.history.navigate.calls[0].args).toContain('search/edx');
|
||||
$('.cancel-button').trigger('click');
|
||||
expect(Backbone.history.navigate.calls[1].args).toContain('');
|
||||
}
|
||||
|
||||
it ('aborts sent search request', function () {
|
||||
function cancelsSearchRequest () {
|
||||
// send search request to server
|
||||
$('.search-field').val('search string');
|
||||
$('.search-button').trigger('click');
|
||||
// cancel search
|
||||
$('.cancel-button').trigger('click');
|
||||
this.server.respondWith([200, {}, JSON.stringify({
|
||||
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: ''
|
||||
}
|
||||
}]
|
||||
})]);
|
||||
this.server.respond();
|
||||
// there should be no results
|
||||
expect($('#course-content')).toBeVisible();
|
||||
expect($('#courseware-search-results')).toBeHidden();
|
||||
});
|
||||
expect(this.$contentElement).toBeVisible();
|
||||
expect(this.$searchResults).toBeHidden();
|
||||
}
|
||||
|
||||
it ('clears results', function () {
|
||||
function clearsResults () {
|
||||
$('.cancel-button').trigger('click');
|
||||
expect($('#course-content')).toBeVisible();
|
||||
expect($('#courseware-search-results')).toBeHidden();
|
||||
});
|
||||
expect(this.$contentElement).toBeVisible();
|
||||
expect(this.$searchResults).toBeHidden();
|
||||
}
|
||||
|
||||
it ('updates navigation history on clear', function () {
|
||||
$('.cancel-button').trigger('click');
|
||||
expect(Backbone.history.fragment).toEqual('');
|
||||
});
|
||||
|
||||
it ('loads next page', function () {
|
||||
function loadsNextPage () {
|
||||
$('.search-field').val('query');
|
||||
$('.search-button').trigger('click');
|
||||
this.server.respondWith([200, {}, JSON.stringify({
|
||||
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: ''
|
||||
}
|
||||
}]
|
||||
})]);
|
||||
this.server.respond();
|
||||
expect(this.$searchResults.find('li').length).toEqual(1);
|
||||
expect($('.search-load-next')).toBeVisible();
|
||||
$('.search-load-next').trigger('click');
|
||||
var body = this.server.requests[1].requestBody;
|
||||
expect(body).toContain('search_string=query');
|
||||
expect(body).toContain('page_index=1');
|
||||
});
|
||||
this.server.respond();
|
||||
expect(this.$searchResults.find('li').length).toEqual(2);
|
||||
}
|
||||
|
||||
it ('navigates to search', function () {
|
||||
function navigatesToSearch () {
|
||||
Backbone.history.loadUrl('search/query');
|
||||
expect(this.server.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(
|
||||
'<section id="courseware-search-results"></section>' +
|
||||
'<section id="course-content"></section>'
|
||||
);
|
||||
loadTemplates.call(this);
|
||||
|
||||
this.server = Sinon.fakeServer.create();
|
||||
var courseId = 'a/b/c';
|
||||
this.app = new CourseSearchApp(
|
||||
courseId,
|
||||
SearchRouter,
|
||||
CourseSearchForm,
|
||||
SearchCollection,
|
||||
CourseSearchResultsView
|
||||
);
|
||||
spyOn(Backbone.history, 'navigate');
|
||||
this.$contentElement = $('#course-content');
|
||||
this.$searchResults = $('#courseware-search-results');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.server.restore();
|
||||
});
|
||||
|
||||
it('shows loading message on search', showsLoadingMessage);
|
||||
it('performs search', performsSearch);
|
||||
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(
|
||||
'<section id="dashboard-search-results"></section>' +
|
||||
'<section id="my-courses"></section>'
|
||||
);
|
||||
loadTemplates.call(this);
|
||||
|
||||
this.server = Sinon.fakeServer.create();
|
||||
this.app = new DashSearchApp(
|
||||
SearchRouter,
|
||||
DashSearchForm,
|
||||
SearchCollection,
|
||||
DashSearchResultsView
|
||||
);
|
||||
|
||||
spyOn(Backbone.history, 'navigate');
|
||||
this.$contentElement = $('#my-courses');
|
||||
this.$searchResults = $('#dashboard-search-results');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.server.restore();
|
||||
});
|
||||
|
||||
it('shows loading message on search', showsLoadingMessage);
|
||||
it('performs search', performsSearch);
|
||||
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 () {
|
||||
$('.search-field').val('search string');
|
||||
$('.search-button').trigger('click');
|
||||
this.server.respondWith([200, {}, JSON.stringify({
|
||||
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: ''
|
||||
}
|
||||
}]
|
||||
})]);
|
||||
this.server.respond();
|
||||
expect($('.search-back-to-courses')).toExist();
|
||||
$('.search-back-to-courses').trigger('click');
|
||||
expect(this.$contentElement).toBeVisible();
|
||||
expect(this.$searchResults).toBeHidden();
|
||||
expect(this.$searchResults).toBeEmpty();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user