mirror of
https://github.com/nasa/openmct.git
synced 2025-04-13 22:23:13 +00:00
[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:
parent
ec7e6cc5b4
commit
76151d09a0
@ -27,9 +27,6 @@
|
||||
define(function () {
|
||||
"use strict";
|
||||
|
||||
var INITIAL_LOAD_NUMBER = 20,
|
||||
LOAD_INCREMENT = 20;
|
||||
|
||||
/**
|
||||
* Controller for search in Tree View.
|
||||
*
|
||||
@ -50,9 +47,8 @@ define(function () {
|
||||
var controller = this;
|
||||
this.$scope = $scope;
|
||||
this.searchService = searchService;
|
||||
this.numberToDisplay = INITIAL_LOAD_NUMBER;
|
||||
this.fullResults = [];
|
||||
this.filteredResults = [];
|
||||
this.numberToDisplay = this.RESULTS_PER_PAGE;
|
||||
this.availabileResults = 0;
|
||||
this.$scope.results = [];
|
||||
this.$scope.loading = false;
|
||||
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
|
||||
* for the current query and filters.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
SearchController.prototype.loadMore = function () {
|
||||
this.numberToDisplay += LOAD_INCREMENT;
|
||||
this.updateResults();
|
||||
this.numberToDisplay += this.RESULTS_PER_PAGE;
|
||||
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 () {
|
||||
var inputText = this.$scope.ngModel.input,
|
||||
controller = this;
|
||||
var inputText = this.$scope.ngModel.input;
|
||||
|
||||
this.clearResults();
|
||||
|
||||
@ -96,51 +94,64 @@ define(function () {
|
||||
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.
|
||||
}
|
||||
|
||||
this.pendingQuery = inputText;
|
||||
this.pendingQuery = queryId;
|
||||
|
||||
this
|
||||
.searchService
|
||||
.query(inputText, 60) // TODO: allow filter in search service.
|
||||
.query(inputText, this.numberToDisplay, this.filterPredicate())
|
||||
.then(function (results) {
|
||||
if (controller.pendingQuery !== inputText) {
|
||||
if (controller.pendingQuery !== queryId) {
|
||||
return; // another query in progress, so skip this one.
|
||||
}
|
||||
controller.onSearchComplete(results);
|
||||
});
|
||||
};
|
||||
|
||||
SearchController.prototype.filter = SearchController.prototype.onFilterChange;
|
||||
|
||||
/**
|
||||
* Refilter results and update visible results when filters have changed.
|
||||
*/
|
||||
SearchController.prototype.onFilterChange = function () {
|
||||
this.filter();
|
||||
this.updateVisibleResults();
|
||||
this.pendingQuery = undefined;
|
||||
this.search();
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter `fullResults` based on currenly active filters, storing the result
|
||||
* in `filteredResults`.
|
||||
* Returns a predicate function that can be used to filter object models.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SearchController.prototype.filter = function () {
|
||||
var includeTypes = this.$scope.ngModel.checked;
|
||||
|
||||
SearchController.prototype.filterPredicate = function () {
|
||||
if (this.$scope.ngModel.checkAll) {
|
||||
this.filteredResults = this.fullResults;
|
||||
return;
|
||||
return function () {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
this.filteredResults = this.fullResults.filter(function (hit) {
|
||||
return includeTypes[hit.object.getModel().type];
|
||||
});
|
||||
var includeTypes = this.$scope.ngModel.checked;
|
||||
return function (model) {
|
||||
return !!includeTypes[model.type];
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clear the search results.
|
||||
*
|
||||
@ -148,35 +159,22 @@ define(function () {
|
||||
*/
|
||||
SearchController.prototype.clearResults = function () {
|
||||
this.$scope.results = [];
|
||||
this.fullResults = [];
|
||||
this.filteredResults = [];
|
||||
this.numberToDisplay = INITIAL_LOAD_NUMBER;
|
||||
this.availableResults = 0;
|
||||
this.numberToDisplay = this.RESULTS_PER_PAGE;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Update search results from given `results`.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SearchController.prototype.onSearchComplete = function (results) {
|
||||
this.fullResults = results.hits;
|
||||
this.filter();
|
||||
this.updateVisibleResults();
|
||||
this.availableResults = results.total;
|
||||
this.$scope.results = results.hits;
|
||||
this.$scope.loading = false;
|
||||
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;
|
||||
});
|
||||
|
@ -4,12 +4,12 @@
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* 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
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* 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
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
@ -24,185 +24,215 @@
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define(
|
||||
["../../src/controllers/SearchController"],
|
||||
function (SearchController) {
|
||||
"use strict";
|
||||
define([
|
||||
'../../src/controllers/SearchController'
|
||||
], function (
|
||||
SearchController
|
||||
) {
|
||||
'use strict';
|
||||
|
||||
// These should be the same as the ones on the top of the search controller
|
||||
var INITIAL_LOAD_NUMBER = 20,
|
||||
LOAD_INCREMENT = 20;
|
||||
|
||||
describe("The search controller", function () {
|
||||
var mockScope,
|
||||
mockSearchService,
|
||||
mockPromise,
|
||||
mockSearchResult,
|
||||
mockDomainObject,
|
||||
mockTypes,
|
||||
controller;
|
||||
describe('The search controller', function () {
|
||||
var mockScope,
|
||||
mockSearchService,
|
||||
mockPromise,
|
||||
mockSearchResult,
|
||||
mockDomainObject,
|
||||
mockTypes,
|
||||
controller;
|
||||
|
||||
function bigArray(size) {
|
||||
var array = [],
|
||||
i;
|
||||
for (i = 0; i < size; i += 1) {
|
||||
array.push(mockSearchResult);
|
||||
}
|
||||
return array;
|
||||
function bigArray(size) {
|
||||
var array = [],
|
||||
i;
|
||||
for (i = 0; i < size; i += 1) {
|
||||
array.push(mockSearchResult);
|
||||
}
|
||||
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$watch" ]
|
||||
);
|
||||
mockScope.ngModel = {};
|
||||
mockScope.ngModel.input = "test input";
|
||||
mockScope.ngModel.checked = {};
|
||||
mockScope.ngModel.checked['mock.type'] = true;
|
||||
return array;
|
||||
}
|
||||
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
'$scope',
|
||||
[ '$watch' ]
|
||||
);
|
||||
mockScope.ngModel = {};
|
||||
mockScope.ngModel.input = 'test input';
|
||||
mockScope.ngModel.checked = {};
|
||||
mockScope.ngModel.checked['mock.type'] = true;
|
||||
mockScope.ngModel.checkAll = true;
|
||||
|
||||
mockSearchService = jasmine.createSpyObj(
|
||||
'searchService',
|
||||
[ 'query' ]
|
||||
);
|
||||
mockPromise = jasmine.createSpyObj(
|
||||
'promise',
|
||||
[ 'then' ]
|
||||
);
|
||||
mockSearchService.query.andReturn(mockPromise);
|
||||
|
||||
mockTypes = [{key: 'mock.type', name: 'Mock Type', glyph: '?'}];
|
||||
|
||||
mockSearchResult = jasmine.createSpyObj(
|
||||
'searchResult',
|
||||
[ '' ]
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
[ 'getModel' ]
|
||||
);
|
||||
mockSearchResult.object = mockDomainObject;
|
||||
mockDomainObject.getModel.andReturn({name: 'Mock Object', type: 'mock.type'});
|
||||
|
||||
controller = new SearchController(mockScope, mockSearchService, mockTypes);
|
||||
controller.search();
|
||||
});
|
||||
|
||||
it('has a default number of results per page', function () {
|
||||
expect(controller.RESULTS_PER_PAGE).toBe(20);
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
mockSearchService = jasmine.createSpyObj(
|
||||
"searchService",
|
||||
[ "query" ]
|
||||
);
|
||||
mockPromise = jasmine.createSpyObj(
|
||||
"promise",
|
||||
[ "then" ]
|
||||
);
|
||||
mockSearchService.query.andReturn(mockPromise);
|
||||
|
||||
mockTypes = [{key: 'mock.type', name: 'Mock Type', glyph: '?'}];
|
||||
|
||||
mockSearchResult = jasmine.createSpyObj(
|
||||
"searchResult",
|
||||
[ "" ]
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[ "getModel" ]
|
||||
);
|
||||
mockSearchResult.object = mockDomainObject;
|
||||
mockDomainObject.getModel.andReturn({name: 'Mock Object', type: 'mock.type'});
|
||||
|
||||
controller = new SearchController(mockScope, mockSearchService, mockTypes);
|
||||
controller.search();
|
||||
});
|
||||
|
||||
it("sends queries to the search service", function () {
|
||||
expect(mockSearchService.query).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("populates the results with results from the search service", function () {
|
||||
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: []});
|
||||
|
||||
expect(mockScope.results).toBeDefined();
|
||||
});
|
||||
|
||||
it("is loading until the service's promise fufills", function () {
|
||||
// Send query
|
||||
controller.search();
|
||||
expect(mockScope.loading).toBeTruthy();
|
||||
|
||||
// Then resolve the promises
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: []});
|
||||
expect(mockScope.loading).toBeFalsy();
|
||||
controller.onFilterChange();
|
||||
var filterFn = mockSearchService.query.mostRecentCall.args[2];
|
||||
expect(filterFn('askbfa')).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it("displays only some results when there are many", function () {
|
||||
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: bigArray(100)});
|
||||
|
||||
expect(mockScope.results).toBeDefined();
|
||||
expect(mockScope.results.length).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it("detects when there are more results", function () {
|
||||
it('returns true only for matching checked types', function () {
|
||||
mockScope.ngModel.checkAll = false;
|
||||
|
||||
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
||||
mockPromise.then.mostRecentCall.args[0]({
|
||||
hits: bigArray(INITIAL_LOAD_NUMBER + 5),
|
||||
total: INITIAL_LOAD_NUMBER + 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 () {
|
||||
var oldSize;
|
||||
|
||||
expect(mockPromise.then).toHaveBeenCalled();
|
||||
mockPromise.then.mostRecentCall.args[0]({
|
||||
hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1),
|
||||
total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
|
||||
});
|
||||
// These hits and total lengths are the case where the controller
|
||||
// DOES NOT have to re-search to load more results
|
||||
oldSize = mockScope.results.length;
|
||||
|
||||
expect(controller.areMore()).toBeTruthy();
|
||||
|
||||
controller.loadMore();
|
||||
expect(mockScope.results.length).toBeGreaterThan(oldSize);
|
||||
});
|
||||
|
||||
it("can re-search to load more results", function () {
|
||||
var oldSize,
|
||||
oldCallCount;
|
||||
|
||||
expect(mockPromise.then).toHaveBeenCalled();
|
||||
mockPromise.then.mostRecentCall.args[0]({
|
||||
hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT - 1),
|
||||
total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
|
||||
});
|
||||
// These hits and total lengths are the case where the controller
|
||||
// DOES have to re-search to load more results
|
||||
oldSize = mockScope.results.length;
|
||||
oldCallCount = mockPromise.then.callCount;
|
||||
expect(controller.areMore()).toBeTruthy();
|
||||
|
||||
controller.loadMore();
|
||||
expect(mockPromise.then).toHaveBeenCalled();
|
||||
// Make sure that a NEW call to search has been made
|
||||
expect(oldCallCount).toBeLessThan(mockPromise.then.callCount);
|
||||
mockPromise.then.mostRecentCall.args[0]({
|
||||
hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1),
|
||||
total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
|
||||
});
|
||||
expect(mockScope.results.length).toBeGreaterThan(oldSize);
|
||||
});
|
||||
|
||||
it("sets the ngModel.search flag", function () {
|
||||
// Flag should be true with nonempty input
|
||||
expect(mockScope.ngModel.search).toEqual(true);
|
||||
|
||||
// Flag should be flase with empty input
|
||||
mockScope.ngModel.input = "";
|
||||
controller.search();
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
|
||||
expect(mockScope.ngModel.search).toEqual(false);
|
||||
|
||||
// Both the empty string and undefined should be 'empty input'
|
||||
mockScope.ngModel.input = undefined;
|
||||
controller.search();
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
|
||||
expect(mockScope.ngModel.search).toEqual(false);
|
||||
});
|
||||
|
||||
it("has a default results list to filter from", function () {
|
||||
expect(mockScope.ngModel.filter()).toBeDefined();
|
||||
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));
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: ['a']});
|
||||
|
||||
expect(mockScope.results.length).toBe(1);
|
||||
expect(mockScope.results).toContain('a');
|
||||
});
|
||||
|
||||
it('is loading until the service\'s promise fufills', function () {
|
||||
expect(mockScope.loading).toBeTruthy();
|
||||
|
||||
// Then resolve the promises
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: []});
|
||||
expect(mockScope.loading).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
xit('displays only some results when there are many', function () {
|
||||
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: bigArray(100)});
|
||||
|
||||
expect(mockScope.results).toBeDefined();
|
||||
expect(mockScope.results.length).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it('detects when there are more results', function () {
|
||||
mockPromise.then.mostRecentCall.args[0]({
|
||||
hits: bigArray(controller.RESULTS_PER_PAGE),
|
||||
total: controller.RESULTS_PER_PAGE + 5
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
expect(mockPromise.then).toHaveBeenCalled();
|
||||
mockPromise.then.mostRecentCall.args[0]({
|
||||
hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1),
|
||||
total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
|
||||
});
|
||||
// These hits and total lengths are the case where the controller
|
||||
// DOES NOT have to re-search to load more results
|
||||
oldSize = mockScope.results.length;
|
||||
|
||||
expect(controller.areMore()).toBeTruthy();
|
||||
|
||||
controller.loadMore();
|
||||
expect(mockScope.results.length).toBeGreaterThan(oldSize);
|
||||
});
|
||||
|
||||
xit('can re-search to load more results', function () {
|
||||
var oldSize,
|
||||
oldCallCount;
|
||||
|
||||
expect(mockPromise.then).toHaveBeenCalled();
|
||||
mockPromise.then.mostRecentCall.args[0]({
|
||||
hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT - 1),
|
||||
total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
|
||||
});
|
||||
// These hits and total lengths are the case where the controller
|
||||
// DOES have to re-search to load more results
|
||||
oldSize = mockScope.results.length;
|
||||
oldCallCount = mockPromise.then.callCount;
|
||||
expect(controller.areMore()).toBeTruthy();
|
||||
|
||||
controller.loadMore();
|
||||
expect(mockPromise.then).toHaveBeenCalled();
|
||||
// Make sure that a NEW call to search has been made
|
||||
expect(oldCallCount).toBeLessThan(mockPromise.then.callCount);
|
||||
mockPromise.then.mostRecentCall.args[0]({
|
||||
hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1),
|
||||
total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
|
||||
});
|
||||
expect(mockScope.results.length).toBeGreaterThan(oldSize);
|
||||
});
|
||||
|
||||
it('sets the ngModel.search flag', function () {
|
||||
// Flag should be true with nonempty input
|
||||
expect(mockScope.ngModel.search).toEqual(true);
|
||||
|
||||
// Flag should be flase with empty input
|
||||
mockScope.ngModel.input = '';
|
||||
controller.search();
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
|
||||
expect(mockScope.ngModel.search).toEqual(false);
|
||||
|
||||
// Both the empty string and undefined should be 'empty input'
|
||||
mockScope.ngModel.input = undefined;
|
||||
controller.search();
|
||||
mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
|
||||
expect(mockScope.ngModel.search).toEqual(false);
|
||||
});
|
||||
|
||||
it('attaches a filter function to scope', function () {
|
||||
expect(mockScope.ngModel.filter).toEqual(jasmine.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user