added tests for route wrapping

This commit is contained in:
Spencer Alger 2014-07-15 12:45:45 -07:00
parent 2f4d4b59f7
commit a32d0438b2
10 changed files with 360 additions and 147 deletions

View file

@ -57,7 +57,7 @@ define(function (require) {
};
},
isNumeric: function (v) {
return !_.isNaN(v) && !_.isArray(v) && !_.isNaN(parseInt(v, 10));
return !_.isNaN(v) && (typeof v === 'number' || (!_.isArray(v) && !_.isNaN(parseInt(v, 10))));
},
setValue: function (obj, name, value) {
var path = name.split('.');
@ -75,6 +75,32 @@ define(function (require) {
recurse();
}
}());
},
// limit the number of arguments that are passed to the function
limit: function (context, fn, count) {
// syntax without context limit(fn, 1)
if (count == null && _.isNumeric(fn)) {
count = fn;
fn = context;
context = null;
}
count = count || 0;
if (count > 3) {
// catch all version
return function () {
return fn.apply(context, [].slice.call(arguments, 0, count));
};
}
// shortcuts for common path
return function (a, b, c) {
if (count === 0) return fn.call(context);
if (count === 1) return fn.call(context, a);
if (count === 2) return fn.call(context, a, b);
if (count === 3) return fn.call(context, a, b, c);
};
}
});

View file

@ -1,7 +1,8 @@
define(function (require) {
var _ = require('lodash');
var name = function (construct) {
return construct.name || construct.toString().split('\n').shift();
var name = function (fn) {
return fn.name || fn.toString().split('\n').shift();
};
/**
@ -13,26 +14,44 @@ define(function (require) {
return privPath.map(name).join(' -> ');
};
var module = require('modules').get('kibana/services');
// uniq ids for every module, across instances
var nextId = (function () {
var i = 0;
return function () { return 'pm_' + i++; };
}());
var module = require('modules').get('kibana/utils');
module.service('Private', function ($injector) {
return function Private(construct) {
if (typeof construct !== 'function') {
throw new TypeError('Expected private module "' + construct + '" to be a function');
// one cache per instance of the Private service
var cache = {};
function Private(fn) {
if (typeof fn !== 'function') {
throw new TypeError('Expected private module "' + fn + '" to be a function');
}
var circular = !!(~privPath.indexOf(construct));
if (circular) throw new Error('Circluar refrence to "' + name(construct) + '" found while resolving private deps: ' + pathToString());
var id = fn.$$id;
if (id && cache[id]) return cache[id];
privPath.push(construct);
if (!construct.$$instance) {
var instance = {};
construct.$$instance = $injector.invoke(construct, instance);
construct.$$instance = construct.$$instance || instance;
if (!id) id = fn.$$id = nextId();
else if (~privPath.indexOf(id)) {
throw new Error(
'Circluar refrence to "' + name(fn) + '"' +
' found while resolving private deps: ' + pathToString()
);
}
privPath.push(id);
var context = {};
var instance = $injector.invoke(fn, context);
// if the function returned an instance of something, use that. Otherwise use the context
if (!_.isObject(instance)) instance = context;
privPath.pop();
return construct.$$instance;
};
cache[id] = instance;
return instance;
}
});
});

View file

@ -8,6 +8,11 @@ define(function (require) {
var fullDefers = [];
q.limit = 0;
Object.defineProperty(q, 'length', {
get: function () {
return work.length;
}
});
var checkIfFull = function () {
if (work.length >= q.limit) {
@ -41,7 +46,6 @@ define(function (require) {
work.push(defer);
checkIfFull();
};
}
return WorkQueue;

View file

@ -4,30 +4,27 @@ define(function (require) {
var WorkQueue = require('utils/routes/_work_queue');
return function (route) {
if (!route.resolve && route.redirectTo) {
return;
}
function wrapRouteWithPrep(route) {
if (!route.resolve && route.redirectTo) return;
var userWork = new WorkQueue();
// the point at which we will consider the queue "full"
userWork.limit = _.keys(route.resolve).length;
var resolve = {
__prep__: function (Promise, Private, config, kbnSetup) {
var setup = Private(require('utils/routes/_setup'));
return setup.routeSetupWork()
__prep__: function (Promise, $route, $injector, Notifier) {
return $injector.invoke(wrapRouteWithPrep._oneTimeSetup).then(function () {
return $injector.invoke(wrapRouteWithPrep._setupComplete);
})
.then(function () {
// wait for the queue to fill up, then do all the work
var defer = Promise.defer();
userWork.resolveWhenFull(defer);
defer.promise.then(function () {
return defer.promise.then(function () {
return Promise.all(userWork.doWork());
});
return defer.promise;
})
.catch(function (err) {
// discard any remaining user work
@ -51,5 +48,37 @@ define(function (require) {
// we're copied everything over so now overwrite
route.resolve = resolve;
}
// broken out so that it can be tested
wrapRouteWithPrep._oneTimeSetup = function ($q, kbnSetup, config) {
var prom = $q.all([
kbnSetup(),
config.init(),
]);
// override setup to only return the promise
wrapRouteWithPrep._oneTimeSetup = function () { return prom; };
return prom;
};
// broken out so that it can be tested
wrapRouteWithPrep._setupComplete = function ($route, indexPatterns, config) {
if (!$route.current.$$route.originalPath.match(/settings\/indices/)) {
// always check for existing ids first
return indexPatterns.getIds()
.then(function (patterns) {
if (!patterns || patterns.length === 0) {
throw new errors.NoDefinedIndexPatterns();
}
if (!config.get('defaultIndex')) {
throw new NoDefaultIndexPattern();
}
});
}
};
return wrapRouteWithPrep;
});

View file

@ -21,7 +21,8 @@
test_utils: '../../test/utils',
fixtures: '../../test/unit/fixtures',
specs: '../../test/unit/specs',
sinon: '../../test/utils/sinon'
sinon: '../../test/utils/sinon',
bluebird: '../bower_components/bluebird/js/browser/bluebird'
},
shim: {
'sinon/sinon': {

View file

@ -0,0 +1,26 @@
define(function (require) {
var angular = require('angular');
var sinon = require('test_utils/auto_release_sinon');
/**
* Wrap a test function with the logic required to get the routeProvider
* @param {Function} fn [description]
* @return {[type]} [description]
*/
return function getRouteProvider() {
var $routeProvider;
angular.module('_Temp_Module_', ['ngRoute'])
.config(['$routeProvider', function (_r) {
$routeProvider = _r;
}]);
module('_Temp_Module_');
inject(function () {});
sinon.stub($routeProvider, 'otherwise');
sinon.stub($routeProvider, 'when');
return $routeProvider;
};
});

View file

@ -1,8 +1,10 @@
define(function (require) {
var angular = require('angular');
var _ = require('lodash');
var RouteManager = require('routes').RouteManager;
var routes; // will contain an new instance of RouteManager for each test
var sinon = require('test_utils/auto_release_sinon');
var getRouteProvider = require('./_get_route_provider');
var chainableMethods = [
{ name: 'when', args: ['', {}] },
{ name: 'otherwise', args: [{}] },
@ -10,96 +12,91 @@ define(function (require) {
];
describe('Custom Route Management', function () {
beforeEach(function () {
routes = new RouteManager();
});
describe('top level api', function () {
beforeEach(function () {
routes = new RouteManager();
it('should have chainable methods: ' + _.pluck(chainableMethods, 'name').join(', '), function () {
chainableMethods.forEach(function (meth) {
expect(routes[meth.name].apply(routes, _.clone(meth.args))).to.be(routes);
});
});
it('should have chainable methods: ' + _.pluck(chainableMethods, 'name').join(', '), function () {
chainableMethods.forEach(function (meth) {
expect(routes[meth.name].apply(routes, _.clone(meth.args))).to.be(routes);
describe('#otherwise', function () {
it('should forward the last otherwise route', function () {
var $rp = getRouteProvider();
var otherRoute = {};
routes.otherwise({});
routes.otherwise(otherRoute);
routes.config($rp);
expect($rp.otherwise.callCount).to.be(1);
expect($rp.otherwise.getCall(0).args[0]).to.be(otherRoute);
});
});
describe('#when', function () {
it('should merge the additions into the when() defined routes', function () {
var $rp = getRouteProvider();
routes.when('/some/route');
routes.when('/some/other/route');
// add the addition resolve to every route
routes.addResolves(/.*/, {
addition: function () {}
});
routes.config($rp);
// should have run once for each when route
expect($rp.when.callCount).to.be(2);
expect($rp.otherwise.callCount).to.be(0);
// every route should have the "addition" resolve
expect($rp.when.getCall(0).args[1].resolve.addition).to.be.a('function');
expect($rp.when.getCall(1).args[1].resolve.addition).to.be.a('function');
});
});
describe('#config', function () {
it('should add defined routes to the global $routeProvider service in order', function () {
var $rp = getRouteProvider();
var args = [
['/one', {}],
['/two', {}]
];
args.forEach(function (a) {
routes.when(a[0], a[1]);
});
routes.config($rp);
expect($rp.when.callCount).to.be(args.length);
_.times(args.length, function (i) {
var call = $rp.when.getCall(i);
var a = args.shift();
expect(call.args[0]).to.be(a[0]);
expect(call.args[1]).to.be(a[1]);
});
});
describe('#otherwise', function () {
it('should forward the last otherwise route', function () {
var otherRoute = {};
routes.otherwise({});
routes.otherwise(otherRoute);
it('sets route.reloadOnSearch to false by default', function () {
var $rp = getRouteProvider();
routes.when('/nothing-set');
routes.when('/no-reload', { reloadOnSearch: false });
routes.when('/always-reload', { reloadOnSearch: true });
var exec;
routes.config({
otherwise: function (route) {
expect(route).to.be(otherRoute);
exec = true;
}
});
var exec = 0;
expect(exec).to.be.ok();
});
});
routes.config($rp);
describe('#when', function () {
it('should merge the additions into the when() defined routes', function () {
routes.when('/some/route');
routes.when('/some/other/route');
// add the addition resolve to every route
routes.addResolves(/.*/, {
addition: function () {}
});
var exec = 0;
routes.config({
when: function (path, route) {
exec ++;
// every route should have the "addition" resolve
expect(route.resolve.addition).to.be.a('function');
}
});
// we expect two routes to be sent to the $routeProvider
expect(exec).to.be(2);
});
});
describe('#config', function () {
it('should add defined routes to the global $routeProvider service in order', function () {
var args = [
['/one', {}],
['/two', {}]
];
args.forEach(function (a) {
routes.when(a[0], a[1]);
});
routes.config({
when: function (path, route) {
var a = args.shift();
expect(path).to.be(a[0]);
expect(route).to.be(a[1]);
}
});
});
it('sets route.reloadOnSearch to false by default', function () {
routes.when('/nothing-set');
routes.when('/no-reload', { reloadOnSearch: false });
routes.when('/always-reload', { reloadOnSearch: true });
var exec = 0;
routes.config({
when: function (path, route) {
exec ++;
// true for the one route, false for all others
expect(route.reloadOnSearch).to.be(path === '/always-reload');
}
});
// we expect two routes to be sent to the $routeProvider
expect(exec).to.be(3);
});
expect($rp.when.callCount).to.be(3);
expect($rp.when.firstCall.args[1]).to.have.property('reloadOnSearch', false);
expect($rp.when.secondCall.args[1]).to.have.property('reloadOnSearch', false);
expect($rp.when.lastCall.args[1]).to.have.property('reloadOnSearch', true);
});
});

View file

@ -1,20 +1,94 @@
define(function (require) {
var _ = require('lodash');
var WorkQueue = require('utils/routes/_work_queue');
var sinon = require('test_utils/auto_release_sinon');
require('services/promises');
require('angular').module('UtilsRouteWorkQueueTests', ['kibana/services']);
return function () {
describe('work queue', function () {
var queue;
var Promise;
beforeEach(module('UtilsRouteWorkQueueTests'));
beforeEach(inject(function (_Promise_) {
Promise = _Promise_;
}));
beforeEach(function () { queue = new WorkQueue(); });
afterEach(function () { queue.empty(); });
describe('#push', function () {
it('adds to the interval queue');
it('adds to the interval queue', function () {
queue.push(Promise.defer());
expect(queue).to.have.length(1);
});
});
describe('#resolveWhenFull', function () {
it('resolves requests waiting for the queue to fill when appropriate');
it('resolves requests waiting for the queue to fill when appropriate', function () {
var size = _.random(5, 50);
queue.limit = size;
var whenFull = Promise.defer();
sinon.stub(whenFull, 'resolve');
queue.resolveWhenFull(whenFull);
// push all but one into the queue
_.times(size - 1, function () {
queue.push(Promise.defer());
});
expect(whenFull.resolve.callCount).to.be(0);
queue.push(Promise.defer());
expect(whenFull.resolve.callCount).to.be(1);
queue.empty();
});
});
/**
* Fills the queue with a random number of work defers, but stubs all defer methods
* with the same stub and passed it back.
*
* @param {function} then - called with then(size, stub) so that the test
* can manipulate the filled queue
*/
function fillWithStubs(then) {
var size = _.random(5, 50);
var stub = sinon.stub();
_.times(size, function () {
var d = Promise.defer();
// overwrite the defer methods with the stub
d.resolve = stub;
d.reject = stub;
queue.push(d);
});
then(size, stub);
}
describe('#doWork', function () {
it('flushes the queue and resolves all promises');
it('flushes the queue and resolves all promises', function () {
fillWithStubs(function (size, stub) {
expect(queue).to.have.length(size);
queue.doWork();
expect(queue).to.have.length(0);
expect(stub.callCount).to.be(size);
});
});
});
describe('#empty()', function () {
it('empties the internal queue');
it('empties the internal queue WITHOUT resolving any promises', function () {
fillWithStubs(function (size, stub) {
expect(queue).to.have.length(size);
queue.empty();
expect(queue).to.have.length(0);
expect(stub.callCount).to.be(0);
});
});
});
});
};

View file

@ -1,42 +1,45 @@
define(function (require) {
var sinon = require('test_utils/auto_release_sinon');
var routes = require('routes');
var getRouteProvider = require('./_get_route_provider');
var wrapRouteWithPrep = require('utils/routes/_wrap_route_with_prep');
var Promise = require('bluebird');
var _ = require('lodash');
var RouteManager = require('routes').RouteManager;
var routes;
require('utils/private');
var stub = require('test_utils/auto_release_sinon').stub;
return function () {
describe('wrap route with prep work', function () {
describe('wrapRouteWithPrep fn', function () {
require('test_utils/no_digest_promises').activateForSuite();
beforeEach(function () {
routes = new RouteManager();
});
var $injector;
beforeEach(function (_$injector_) { $injector = _$injector_; });
it('creates resolves if none existed', function () {
var exec = 0;
routes.when('/jones', { template: '<picketfence color="white"></picketfence>' });
routes.config({
when: function (path, route) {
exec += 1;
expect(path).to.eql('/jones');
expect(route).to.have.property('resolve');
expect(route.resolve).to.be.an('object');
it('adds a __prep__ resolve, which does some setup work, then some user work', function () {
var i = 0;
var next = function () {
return _.partial(Promise.resolve, i++);
};
stub(wrapRouteWithPrep, 'oneTimeSetup', Promise.resolve);
stub(wrapRouteWithPrep, 'setupComplete', Promise.resolve);
var route = {
resolve: {
userWork1: next,
userWork2: next,
userWork3: next,
userWork4: next
}
});
expect(exec).to.be(1);
});
};
wrapRouteWithPrep(route);
it('adds a __prep__ property to the resolve object', function () {
var exec = 0;
routes.when('/butter', { resolve: { toast: 'burnThatBread' } });
routes.config({
when: function (path, route) {
exec += 1;
expect(route.resolve).to.have.property('__prep__');
}
return Promise.props(_.mapValues(route.resolve, _.limit($injector.invoke, 1)))
.then(function (resolve) {
expect(resolve.__prep__).to.be('delayed_first');
expect(resolve.userWork1).to.be.above(0);
expect(resolve.userWork2).to.be.above(0);
expect(resolve.userWork3).to.be.above(0);
expect(resolve.userWork4).to.be.above(0);
});
expect(exec).to.be(1);
});
var SchedulingTest = function (opts) {
@ -62,7 +65,7 @@ define(function (require) {
$scope = $rootScope.$new();
});
sinon.stub(
stub(
Private(require('utils/routes/_setup')),
'routeSetupWork',
function () {

View file

@ -0,0 +1,34 @@
define(['angular', 'bluebird', 'services/promises'], function (angular, Bluebird) {
/**
* replace the Promise service with Bluebird so that tests
* can use promises without having to call $rootScope.apply()
*
* var nonDigestPromises = require('test_utils/non_digest_promises');
*
* describe('some module that does complex shit with promises', function () {
* beforeEach(nonDigestPromises.activate);
*
* });
*/
var active = false;
angular.module('kibana/services')
.config(function ($provide) {
$provide.decorator('Promise', function ($delegate) {
return active ? Bluebird : $delegate;
});
});
function activate() { active = true; }
function deactivate() { active = false; }
return {
activate: activate,
deactivate: deactivate,
activateForSuite: function () {
before(activate);
after(deactivate);
}
};
});