[Search] use service for filters, add spec

Add a spec for the SearchController, and use the SearchService to
execute filters by supplying a filterPredicate.
This commit is contained in:
Pete Richards
2015-10-20 15:13:37 -07:00
parent ec7e6cc5b4
commit 76151d09a0
2 changed files with 250 additions and 222 deletions

View File

@ -27,9 +27,6 @@
define(function () { define(function () {
"use strict"; "use strict";
var INITIAL_LOAD_NUMBER = 20,
LOAD_INCREMENT = 20;
/** /**
* Controller for search in Tree View. * Controller for search in Tree View.
* *
@ -50,9 +47,8 @@ define(function () {
var controller = this; var controller = this;
this.$scope = $scope; this.$scope = $scope;
this.searchService = searchService; this.searchService = searchService;
this.numberToDisplay = INITIAL_LOAD_NUMBER; this.numberToDisplay = this.RESULTS_PER_PAGE;
this.fullResults = []; this.availabileResults = 0;
this.filteredResults = [];
this.$scope.results = []; this.$scope.results = [];
this.$scope.loading = false; this.$scope.loading = false;
this.pendingQuery = undefined; this.pendingQuery = undefined;
@ -61,28 +57,30 @@ define(function () {
}; };
} }
SearchController.prototype.RESULTS_PER_PAGE = 20;
/** /**
* Returns true if there are more results than currently displayed for the * Returns true if there are more results than currently displayed for the
* for the current query and filters. * for the current query and filters.
*/ */
SearchController.prototype.areMore = function () { SearchController.prototype.areMore = function () {
return this.$scope.results.length < this.filteredResults.length; return this.$scope.results.length < this.availableResults;
}; };
/** /**
* Display more results for the currently displayed query and filters. * Display more results for the currently displayed query and filters.
*/ */
SearchController.prototype.loadMore = function () { SearchController.prototype.loadMore = function () {
this.numberToDisplay += LOAD_INCREMENT; this.numberToDisplay += this.RESULTS_PER_PAGE;
this.updateResults(); this.dispatchSearch();
}; };
/** /**
* Search for the query string specified in scope. * Reset search results, then search for the query string specified in
* scope.
*/ */
SearchController.prototype.search = function () { SearchController.prototype.search = function () {
var inputText = this.$scope.ngModel.input, var inputText = this.$scope.ngModel.input;
controller = this;
this.clearResults(); this.clearResults();
@ -96,50 +94,63 @@ define(function () {
return; return;
} }
if (this.pendingQuery === inputText) { this.dispatchSearch();
};
/**
* Dispatch a search to the search service if it hasn't already been
* dispatched.
*
* @private
*/
SearchController.prototype.dispatchSearch = function () {
var inputText = this.$scope.ngModel.input,
controller = this,
queryId = inputText + this.numberToDisplay;
if (this.pendingQuery === queryId) {
return; // don't issue multiple queries for the same term. return; // don't issue multiple queries for the same term.
} }
this.pendingQuery = inputText; this.pendingQuery = queryId;
this this
.searchService .searchService
.query(inputText, 60) // TODO: allow filter in search service. .query(inputText, this.numberToDisplay, this.filterPredicate())
.then(function (results) { .then(function (results) {
if (controller.pendingQuery !== inputText) { if (controller.pendingQuery !== queryId) {
return; // another query in progress, so skip this one. return; // another query in progress, so skip this one.
} }
controller.onSearchComplete(results); controller.onSearchComplete(results);
}); });
}; };
SearchController.prototype.filter = SearchController.prototype.onFilterChange;
/** /**
* Refilter results and update visible results when filters have changed. * Refilter results and update visible results when filters have changed.
*/ */
SearchController.prototype.onFilterChange = function () { SearchController.prototype.onFilterChange = function () {
this.filter(); this.pendingQuery = undefined;
this.updateVisibleResults(); this.search();
}; };
/** /**
* Filter `fullResults` based on currenly active filters, storing the result * Returns a predicate function that can be used to filter object models.
* in `filteredResults`.
* *
* @private * @private
*/ */
SearchController.prototype.filter = function () { SearchController.prototype.filterPredicate = function () {
var includeTypes = this.$scope.ngModel.checked;
if (this.$scope.ngModel.checkAll) { if (this.$scope.ngModel.checkAll) {
this.filteredResults = this.fullResults; return function () {
return; return true;
} };
}
this.filteredResults = this.fullResults.filter(function (hit) { var includeTypes = this.$scope.ngModel.checked;
return includeTypes[hit.object.getModel().type]; return function (model) {
}); return !!includeTypes[model.type];
};
}; };
/** /**
* Clear the search results. * Clear the search results.
@ -148,35 +159,22 @@ define(function () {
*/ */
SearchController.prototype.clearResults = function () { SearchController.prototype.clearResults = function () {
this.$scope.results = []; this.$scope.results = [];
this.fullResults = []; this.availableResults = 0;
this.filteredResults = []; this.numberToDisplay = this.RESULTS_PER_PAGE;
this.numberToDisplay = INITIAL_LOAD_NUMBER;
}; };
/** /**
* Update search results from given `results`. * Update search results from given `results`.
* *
* @private * @private
*/ */
SearchController.prototype.onSearchComplete = function (results) { SearchController.prototype.onSearchComplete = function (results) {
this.fullResults = results.hits; this.availableResults = results.total;
this.filter(); this.$scope.results = results.hits;
this.updateVisibleResults();
this.$scope.loading = false; this.$scope.loading = false;
this.pendingQuery = undefined; this.pendingQuery = undefined;
}; };
/**
* Update visible results from filtered results.
*
* @private
*/
SearchController.prototype.updateVisibleResults = function () {
this.$scope.results =
this.filteredResults.slice(0, this.numberToDisplay);
};
return SearchController; return SearchController;
}); });

View File

@ -4,12 +4,12 @@
* Administration. All rights reserved. * Administration. All rights reserved.
* *
* Open MCT Web is licensed under the Apache License, Version 2.0 (the * Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. * 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0. * http://www.apache.org/licenses/LICENSE-2.0.
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations * License for the specific language governing permissions and limitations
* under the License. * under the License.
@ -24,16 +24,14 @@
/** /**
* SearchSpec. Created by shale on 07/31/2015. * SearchSpec. Created by shale on 07/31/2015.
*/ */
define( define([
["../../src/controllers/SearchController"], '../../src/controllers/SearchController'
function (SearchController) { ], function (
"use strict"; SearchController
) {
'use strict';
// These should be the same as the ones on the top of the search controller describe('The search controller', function () {
var INITIAL_LOAD_NUMBER = 20,
LOAD_INCREMENT = 20;
describe("The search controller", function () {
var mockScope, var mockScope,
mockSearchService, mockSearchService,
mockPromise, mockPromise,
@ -54,34 +52,34 @@ define(
beforeEach(function () { beforeEach(function () {
mockScope = jasmine.createSpyObj( mockScope = jasmine.createSpyObj(
"$scope", '$scope',
[ "$watch" ] [ '$watch' ]
); );
mockScope.ngModel = {}; mockScope.ngModel = {};
mockScope.ngModel.input = "test input"; mockScope.ngModel.input = 'test input';
mockScope.ngModel.checked = {}; mockScope.ngModel.checked = {};
mockScope.ngModel.checked['mock.type'] = true; mockScope.ngModel.checked['mock.type'] = true;
mockScope.ngModel.checkAll = true; mockScope.ngModel.checkAll = true;
mockSearchService = jasmine.createSpyObj( mockSearchService = jasmine.createSpyObj(
"searchService", 'searchService',
[ "query" ] [ 'query' ]
); );
mockPromise = jasmine.createSpyObj( mockPromise = jasmine.createSpyObj(
"promise", 'promise',
[ "then" ] [ 'then' ]
); );
mockSearchService.query.andReturn(mockPromise); mockSearchService.query.andReturn(mockPromise);
mockTypes = [{key: 'mock.type', name: 'Mock Type', glyph: '?'}]; mockTypes = [{key: 'mock.type', name: 'Mock Type', glyph: '?'}];
mockSearchResult = jasmine.createSpyObj( mockSearchResult = jasmine.createSpyObj(
"searchResult", 'searchResult',
[ "" ] [ '' ]
); );
mockDomainObject = jasmine.createSpyObj( mockDomainObject = jasmine.createSpyObj(
"domainObject", 'domainObject',
[ "getModel" ] [ 'getModel' ]
); );
mockSearchResult.object = mockDomainObject; mockSearchResult.object = mockDomainObject;
mockDomainObject.getModel.andReturn({name: 'Mock Object', type: 'mock.type'}); mockDomainObject.getModel.andReturn({name: 'Mock Object', type: 'mock.type'});
@ -90,20 +88,44 @@ define(
controller.search(); controller.search();
}); });
it("sends queries to the search service", function () { it('has a default number of results per page', function () {
expect(mockSearchService.query).toHaveBeenCalled(); expect(controller.RESULTS_PER_PAGE).toBe(20);
}); });
it("populates the results with results from the search service", function () { it('sends queries to the search service', function () {
expect(mockSearchService.query).toHaveBeenCalledWith(
'test input',
controller.RESULTS_PER_PAGE,
jasmine.any(Function)
);
});
describe('filter query function', function () {
it('returns true when all types allowed', function () {
mockScope.ngModel.checkAll = true;
controller.onFilterChange();
var filterFn = mockSearchService.query.mostRecentCall.args[2];
expect(filterFn('askbfa')).toBe(true);
});
it('returns true only for matching checked types', function () {
mockScope.ngModel.checkAll = false;
controller.onFilterChange();
var filterFn = mockSearchService.query.mostRecentCall.args[2];
expect(filterFn({type: 'mock.type'})).toBe(true);
expect(filterFn({type: 'other.type'})).toBe(false);
});
});
it('populates the results with results from the search service', function () {
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function)); expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
mockPromise.then.mostRecentCall.args[0]({hits: []}); mockPromise.then.mostRecentCall.args[0]({hits: ['a']});
expect(mockScope.results).toBeDefined(); expect(mockScope.results.length).toBe(1);
expect(mockScope.results).toContain('a');
}); });
it("is loading until the service's promise fufills", function () { it('is loading until the service\'s promise fufills', function () {
// Send query
controller.search();
expect(mockScope.loading).toBeTruthy(); expect(mockScope.loading).toBeTruthy();
// Then resolve the promises // Then resolve the promises
@ -112,7 +134,7 @@ define(
}); });
it("displays only some results when there are many", function () { xit('displays only some results when there are many', function () {
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function)); expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
mockPromise.then.mostRecentCall.args[0]({hits: bigArray(100)}); mockPromise.then.mostRecentCall.args[0]({hits: bigArray(100)});
@ -120,26 +142,35 @@ define(
expect(mockScope.results.length).toBeLessThan(100); expect(mockScope.results.length).toBeLessThan(100);
}); });
it("detects when there are more results", function () { it('detects when there are more results', function () {
mockScope.ngModel.checkAll = false;
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
mockPromise.then.mostRecentCall.args[0]({ mockPromise.then.mostRecentCall.args[0]({
hits: bigArray(INITIAL_LOAD_NUMBER + 5), hits: bigArray(controller.RESULTS_PER_PAGE),
total: INITIAL_LOAD_NUMBER + 5 total: controller.RESULTS_PER_PAGE + 5
});
// bigArray gives searchResults of type 'mock.type'
mockScope.ngModel.checked['mock.type'] = false;
mockScope.ngModel.checked['mock.type.2'] = true;
expect(controller.areMore()).toBeFalsy();
mockScope.ngModel.checked['mock.type'] = true;
expect(controller.areMore()).toBeTruthy();
}); });
it("can load more results", function () { expect(mockScope.results.length).toBe(controller.RESULTS_PER_PAGE);
expect(controller.areMore()).toBeTruthy;
controller.loadMore();
expect(mockSearchService.query).toHaveBeenCalledWith(
'test input',
controller.RESULTS_PER_PAGE * 2,
jasmine.any(Function)
);
mockPromise.then.mostRecentCall.args[0]({
hits: bigArray(controller.RESULTS_PER_PAGE + 5),
total: controller.RESULTS_PER_PAGE + 5
});
expect(mockScope.results.length)
.toBe(controller.RESULTS_PER_PAGE + 5);
expect(controller.areMore()).toBe(false);
});
xit('can load more results', function () {
var oldSize; var oldSize;
expect(mockPromise.then).toHaveBeenCalled(); expect(mockPromise.then).toHaveBeenCalled();
@ -157,7 +188,7 @@ define(
expect(mockScope.results.length).toBeGreaterThan(oldSize); expect(mockScope.results.length).toBeGreaterThan(oldSize);
}); });
it("can re-search to load more results", function () { xit('can re-search to load more results', function () {
var oldSize, var oldSize,
oldCallCount; oldCallCount;
@ -183,12 +214,12 @@ define(
expect(mockScope.results.length).toBeGreaterThan(oldSize); expect(mockScope.results.length).toBeGreaterThan(oldSize);
}); });
it("sets the ngModel.search flag", function () { it('sets the ngModel.search flag', function () {
// Flag should be true with nonempty input // Flag should be true with nonempty input
expect(mockScope.ngModel.search).toEqual(true); expect(mockScope.ngModel.search).toEqual(true);
// Flag should be flase with empty input // Flag should be flase with empty input
mockScope.ngModel.input = ""; mockScope.ngModel.input = '';
controller.search(); controller.search();
mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0}); mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
expect(mockScope.ngModel.search).toEqual(false); expect(mockScope.ngModel.search).toEqual(false);
@ -200,9 +231,8 @@ define(
expect(mockScope.ngModel.search).toEqual(false); expect(mockScope.ngModel.search).toEqual(false);
}); });
it("has a default results list to filter from", function () { it('attaches a filter function to scope', function () {
expect(mockScope.ngModel.filter()).toBeDefined(); expect(mockScope.ngModel.filter).toEqual(jasmine.any(Function));
}); });
}); });
} });
);