Merge branch 'develop' into vislib/refactor1

Merging develop
This commit is contained in:
Shelby Sturgis 2014-08-07 21:38:12 +03:00
commit 6f6e054fda
21 changed files with 553 additions and 113 deletions

34
TODOS.md Normal file
View file

@ -0,0 +1,34 @@
# TODO items
> Automatically extracted
- **src/kibana/apps/dashboard/directives/grid.js**
- change this from event based to calling a method on dashboardApp [L68](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/dashboard/directives/grid.js#L68)
- **src/kibana/apps/discover/controllers/discover.js**
- Switch this to watching time.string when we implement it [L148](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js#L148)
- On array fields, negating does not negate the combination, rather all terms [L431](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js#L431)
- Move to utility class [L496](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js#L496)
- Move to utility class [L506](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js#L506)
- **src/kibana/apps/settings/sections/indices/_create.js**
- we should probably display a message of some kind [L111](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/settings/sections/indices/_create.js#L111)
- **src/kibana/apps/visualize/controllers/editor.js**
- Switch this to watching time.string when we implement it [L189](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/controllers/editor.js#L189)
- **src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js**
- Should we abtract out the agg building stuff? [L58](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js#L58)
- Should this be abstracted somewhere? Its a copy/paste from _saved_vis.js [L89](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js#L89)
- **src/kibana/apps/visualize/saved_visualizations/_type_defs.js**
- We need to be able to get ahold of angular services here [L16](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_type_defs.js#L16)
- We need to be able to get ahold of angular services here [L88](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_type_defs.js#L88)
- **src/kibana/apps/visualize/saved_visualizations/bucket_aggs/terms.js**
- We need more just _count here. [L26](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/terms.js#L26)
- **src/kibana/components/index_patterns/_mapper.js**
- Change index to be the resolved in some way, last three months, last hour, last year, whatever [L49](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/index_patterns/_mapper.js#L49)
- **src/kibana/components/state_management/state.js**
- Change all the references to onUpdate to the actual fetch_with_changes event [L72](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/state_management/state.js#L72)
- **src/kibana/directives/rows.js**
- It would be better to actually check the type of the field, but we don't have [L38](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/directives/rows.js#L38)
- **src/kibana/services/timefilter.js**
- This should be disabled on route change, apps need to enable it explicitly [L12](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/services/timefilter.js#L12)
- **test/unit/specs/apps/dashboard/directives/panel.js**
- This should not be needed, timefilter is only included here [L14](https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/apps/dashboard/directives/panel.js#L14)
- **test/unit/specs/directives/timepicker.js**
- This should not be needed, timefilter is only included here, it should move [L17](https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/directives/timepicker.js#L17)

View file

@ -33,7 +33,7 @@
"scripts": {
"test": "grunt test",
"server": "grunt server",
"precommit": "grunt jshint"
"precommit": "grunt jshint todos"
},
"repository": {
"type": "git",

View file

@ -77,7 +77,8 @@ define(function (require) {
query: initialQuery || '',
columns: ['_source'],
index: config.get('defaultIndex'),
interval: 'auto'
interval: 'auto',
filters: _.cloneDeep($scope.searchSource.get('filter'))
};
var metaFields = config.get('metaFields');
@ -93,7 +94,7 @@ define(function (require) {
'year'
];
var $state = $scope.state = new appStateFactory.create(stateDefaults);
var $state = $scope.state = appStateFactory.create(stateDefaults);
if (!_.contains(indexPatternList, $state.index)) {
var reason = 'The index specified in the URL is not a configured pattern. ';
@ -161,6 +162,10 @@ define(function (require) {
if (!angular.equals(sort, currentSort)) $scope.fetch();
});
$scope.$watch('state.filters', function (filters) {
$scope.fetch();
});
$scope.$watch('opts.timefield', function (timefield) {
timefilter.enabled(!!timefield);
});
@ -199,6 +204,8 @@ define(function (require) {
if (!init.complete) return;
$scope.updateTime();
if (_.isEmpty($state.columns)) refreshColumns();
$state.save();
$scope.updateDataSource()
.then(setupVisualization)
.then(function () {
@ -340,7 +347,6 @@ define(function (require) {
$scope.updateDataSource = function () {
var chartOptions;
$scope.searchSource
.size($scope.opts.sampleSize)
.sort(function () {
@ -354,7 +360,8 @@ define(function (require) {
}
return sort;
})
.query(!$state.query ? null : $state.query);
.query(!$state.query ? null : $state.query)
.set('filter', $state.filters || []);
// get the current indexPattern
var indexPattern = $scope.searchSource.get('index');
@ -432,20 +439,26 @@ define(function (require) {
// TODO: On array fields, negating does not negate the combination, rather all terms
$scope.filterQuery = function (field, value, operation) {
value = _.isArray(value) ? value : [value];
operation = operation || '+';
var indexPattern = $scope.searchSource.get('index');
indexPattern.popularizeField(field, 1);
_.each(value, function (clause) {
var filter = field + ':"' + addSlashes(clause) + '"';
var regex = '[\\+-]' + regexEscape(filter) + '\\s*';
// Grap the filters from the searchSource and ensure it's an array
var filters = _.flatten([$state.filters], true);
$state.query = $state.query.replace(new RegExp(regex), '') +
' ' + operation + filter;
_.each(value, function (clause) {
var previous = _.find(filters, function (item) {
return item && item.query.match[field] === clause;
});
if (!previous) {
var filter = { query: { match: {} } };
filter.negate = operation === '-';
filter.query.match[field] = clause;
filters.push(filter);
}
});
$scope.fetch();
$state.filters = filters;
};
$scope.toggleField = function (name) {

View file

@ -24,6 +24,9 @@
<config config-template="configTemplate" config-object="opts" config-close="configClose" config-submit="fetch"></config>
<div class="container-fluid">
<div class="row">
<filter-bar state="state"></filter-bar>
</div>
<div class="row">
<div class="discover-hits"><strong>{{hits || 0}}</strong> hits</div>

View file

@ -19,6 +19,19 @@ define(function (require) {
var serviceObj = registry.get($routeParams.service);
var service = $injector.get(serviceObj.service);
/**
* Creates a field definition and pushes it to the memo stack. This function
* is designed to be used in conjunction with _.reduce(). If the
* values is plain object it will recurse through all the keys till it hits
* a string, number or an array.
*
* @param {array} memo The stack of fields
* @param {mixed} value The value of the field
* @param {stirng} key The key of the field
* @param {object} collection This is a reference the collection being reduced
* @param {array} parents The parent keys to the field
* @returns {array}
*/
var createField = function (memo, val, key, collection, parents) {
if (_.isArray(parents)) {
parents.push(key);
@ -92,6 +105,11 @@ define(function (require) {
});
};
/**
* Deletes an object and sets the notification
* @param {type} name description
* @returns {type} description
*/
$scope.delete = function () {
$scope.obj.delete().then(function (resp) {
$location.path('/settings/objects').search({ _a: rison.encode({
@ -132,4 +150,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -63,7 +63,7 @@ define(function (require) {
$scope.fields = _.sortBy(indexPattern.fields, 'name');
$scope.fields.byName = indexPattern.fieldsByName;
var $state = $scope.state = new appStateFactory.create(vis.getState());
var $state = $scope.state = appStateFactory.create(vis.getState());
if ($state.query) {
vis.searchSource.set('query', $state.query);

View file

@ -247,6 +247,29 @@ define(function (require) {
};
}
/**
* Create a filter that can be reversed for filters with negate set
* @param {boolean} reverse This will reverse the filter. If true then
* anything where negate is set will come
* through otherwise it will filter out
* @returns {function}
*/
var filterNegate = function (reverse) {
return function (filter) {
if (_.isUndefined(filter.negate)) return !reverse;
return filter.negate === reverse;
};
};
/**
* Clean out any invalid attributes from the filters
* @param {object} filter The filter to clean
* @returns {object}
*/
var cleanFilter = function (filter) {
return _.omit(filter, ['negate', 'disabled']);
};
// switch to filtered query if there are filters
if (flatState.filters) {
if (flatState.filters.length) {
@ -255,7 +278,8 @@ define(function (require) {
query: flatState.body.query,
filter: {
bool: {
must: flatState.filters
must: _(flatState.filters).filter(filterNegate(false)).map(cleanFilter).value(),
must_not: _(flatState.filters).filter(filterNegate(true)).map(cleanFilter).value()
}
}
}

View file

@ -157,7 +157,16 @@ define(function (require) {
switch (key) {
case 'filter':
// user a shallow flatten to detect if val is an array, and pull the values out if it is
state.filters = _.flatten([ state.filters || [], val ], true);
state.filters = _([ state.filters || [], val ])
.flatten(true)
// Yo Dawg! I heard you needed to filter out your filters
.filter(function (filter) {
if (!filter) return false;
// return true for anything that is either empty or false
// return false for anything that is explicitly set to true
return !filter.disabled;
})
.value();
return;
case 'index':
case 'type':

View file

@ -0,0 +1,30 @@
filter-bar .bar {
padding: 6px 6px 4px 6px;
background: #dde4e6;
}
filter-bar .bar .title {
display: inline;
color: #748287;
margin: 0 6px;
}
filter-bar .bar .filter {
font-size: 12px;
border-radius: 12px;
display: inline-block;
background-color: #83949C;
padding: 4px 8px;
color: #fff;
margin-right: 4px;
margin-bottom: 4px;
}
filter-bar .bar .filter .value {
border-right: 1px solid rgba(255, 255, 255, 0.4);
padding-right: 8px;
margin-right: 4px;
}
filter-bar .bar .filter.negate {
background-color: #D18282;
}
filter-bar .bar .filter a {
color: #FFF;
}

View file

@ -0,0 +1,9 @@
<div class="bar" ng-show="filters.length">
<!-- <div class="title">Filters</div> -->
<div class="filter" ng-class="{ negate: filter.negate }" ng-repeat="filter in filters">
<span class="key">{{ filter.key }}:</span>
<span class="value">"{{ filter.value }}"</span>
<a class="fa" tooltip="Toggle" tooltip-placement="top" ng-class="{ 'fa-eye-slash': filter.disabled, 'fa-eye': !filter.disabled }" ng-click="toggleFilter(filter)"><a>
<a class="fa fa-times" tooltip="Remove" tooltip-placement="top" ng-click="removeFilter(filter)"><a>
</div>
</div>

View file

@ -0,0 +1,88 @@
define(function (require) {
'use strict';
var _ = require('lodash');
var module = require('modules').get('kibana');
var template = require('text!components/filter_bar/filter_bar.html');
module.directive('filterBar', function (courier) {
return {
restrict: 'E',
template: template,
scope: {
state: '='
},
link: function ($scope, $el, attrs) {
/**
* Map the filter into an object with the key and value exposed so it's
* easier to work with in the template
* @param {object} fitler The filter the map
* @returns {object}
*/
var mapFilter = function (filter) {
var key = _.keys(filter.query.match)[0];
return {
key: key,
value: filter.query.match[key],
disabled: !!(filter.disabled),
negate: !!(filter.negate),
filter: filter
};
};
$scope.$watch('state.filters', function (filters) {
// Get the filters from the searchSource
$scope.filters = _(filters)
.filter(function (filter) {
return filter;
})
.flatten(true)
.map(mapFilter)
.value();
});
/**
* Remap the filter from the intermediary back to it's original.
* @param {object} filter The original filter
* @returns {object}
*/
var remapFilters = function (filter) {
return filter.filter;
};
/**
* Toggles the filter between enabled/disabled.
* @param {object} filter The filter to toggle
* @returns {void}
*/
$scope.toggleFilter = function (filter) {
// Toggle the disabled flag
var disabled = !filter.disabled;
filter.disabled = disabled;
filter.filter.disabled = disabled;
// Save the filters back to the searchSource
$scope.state.filters = _.map($scope.filters, remapFilters);
};
/**
* Removes the filter from the searchSource
* @param {object} filter The filter to remove
* @returns {void}
*/
$scope.removeFilter = function (invalidFilter) {
// Remove the filter from the the scope $filters and map it back
// to the original format to save in searchSource
$scope.state.filters = _($scope.filters)
.filter(function (filter) {
return filter.filter !== invalidFilter.filter;
})
.map(remapFilters)
.value();
};
}
};
});
});

View file

@ -0,0 +1,35 @@
filter-bar .bar {
padding: 6px 6px 4px 6px;
background: #dde4e6;
.title {
display: inline;
color: #748287;
margin: 0 6px;
}
.filter {
font-size: 12px;
border-radius: 12px;
display: inline-block;
background-color: #83949C;
padding: 4px 8px;
color: #fff;
margin-right: 4px;
margin-bottom: 4px;
.value {
border-right: 1px solid rgba(255, 255, 255, 0.4);
padding-right: 8px;
margin-right: 4px;
}
&.negate {
background-color: #D18282;
}
a {
color: #FFF;
}
}
}

View file

@ -11,6 +11,7 @@ define(function (require) {
var getAppStash = function (search) {
var appStash = search._a && rison.decode(search._a);
if (app.current) {
// Apply the defaults to appStash
appStash = _.defaults(appStash || {}, app.defaults);
}
return appStash;
@ -137,4 +138,4 @@ define(function (require) {
};
};
};
});
});

View file

@ -9,6 +9,7 @@ define(function (require) {
AppState.Super.call(this, '_a', defaults);
}
return AppState;
};

View file

@ -10,6 +10,7 @@ define(function (require) {
require('components/courier/courier');
require('components/notify/notify');
require('components/state_management/app_state_factory');
require('components/filter_bar/filter_bar');
require('directives/info');
require('directives/spinner');
require('directives/paginate');

View file

@ -1,8 +1,9 @@
define(function (require) {
var _ = require('lodash');
return function EventsProvider(Private, PromiseEmitter) {
return function EventsProvider(Private, Promise, Notifier) {
var BaseObject = Private(require('factories/base_object'));
var notify = new Notifier({ location: 'EventEmitter' });
_.inherits(Events, BaseObject);
function Events() {
@ -12,28 +13,42 @@ define(function (require) {
/**
* Listens for events
* @param {string} name The name of the event
* @param {function} handler The handler for the event
* @returns {PromiseEmitter}
* @param {string} name - The name of the event
* @param {function} handler - The function to call when the event is triggered
* @returns {undefined}
*/
Events.prototype.on = function (name, handler) {
var self = this;
if (!_.isArray(this._listeners[name])) {
this._listeners[name] = [];
}
return new PromiseEmitter(function (resolve, reject, defer) {
defer._handler = handler;
self._listeners[name].push(defer);
}, handler);
var listener = { handler: handler };
// capture the promise that is resolved when listener.defer is "fresh"/new
// and attach it to the listener
(function buildDefer(value) {
// we will execute the handler on each re-build, but not the initial build
var rebuilding = listener.defer != null;
listener.defer = Promise.defer();
listener.deferResolved = false;
listener.newDeferPromise = listener.defer.promise.then(buildDefer);
if (!rebuilding) return;
// we ignore the completion of handlers, just watch for unhandled errors
Promise.try(handler, [value]).catch(notify.fatal);
}());
this._listeners[name].push(listener);
};
/**
* Removes a event listner
* @param {string} name The name of the event
* @param {function} [handler] The handler to remove
* @return {void}
* Removes an event listener
* @param {string} [name] - The name of the event
* @param {function} [handler] - The handler to remove
* @return {undefined}
*/
Events.prototype.off = function (name, handler) {
if (!name && !handler) {
@ -47,31 +62,38 @@ define(function (require) {
if (!handler) {
delete this._listeners[name];
} else {
this._listeners[name] = _.filter(this._listeners[name], function (defer) {
return handler !== defer._handler;
this._listeners[name] = _.filter(this._listeners[name], function (listener) {
return handler !== listener.handler;
});
}
};
/**
* Emits and event using the PromiseEmitter
* @param {string} name The name of the event
* @param {mixed} args The args to pass along to the handers
* @returns {void}
* Emits the event to all listeners
*
* @param {string} name - The name of the event.
* @param {any} [value] - The value that will be passed to all event handlers.
* @returns {Promise}
*/
Events.prototype.emit = function () {
var args = Array.prototype.slice.call(arguments);
var name = args.shift();
if (this._listeners[name]) {
// We need to empty the array when we resolve the listners. PromiseEmitter
// will regenerate the listners array with new promises.
_.each(this._listeners[name].splice(0), function (defer) {
defer.resolve.apply(defer, args);
});
Events.prototype.emit = function (name, value) {
if (!this._listeners[name]) {
return Promise.resolve();
}
return Promise.map(this._listeners[name], function resolveListener(listener) {
if (listener.deferResolved) {
// this listener has already been resolved by another call to events#emit()
// so we wait for listener.defer to be recreated and try again
return listener.newDeferPromise.then(function () {
return resolveListener(listener);
});
} else {
listener.deferResolved = true;
listener.defer.resolve(value);
}
});
};
return Events;
};
});

View file

@ -347,3 +347,4 @@ input[type="checkbox"],
}
}
@import '../components/filter_bar/filter_bar.less';

View file

@ -9,7 +9,8 @@ module.exports = {
'<%= src %>/kibana/apps/settings/styles/main.less',
'<%= src %>/kibana/apps/visualize/styles/main.less',
'<%= src %>/kibana/apps/visualize/styles/visualization.less',
'<%= src %>/kibana/styles/main.less'
'<%= src %>/kibana/styles/main.less',
'<%= src %>/kibana/components/**/*.less'
],
expand: true,
ext: '.css',
@ -18,4 +19,4 @@ module.exports = {
paths: [bc + '/lesshat/build/']
}
}
};
};

View file

@ -8,7 +8,8 @@ module.exports = function (grunt) {
},
less: {
files: [
'<%= app %>/**/styles/**/*.less'
'<%= app %>/**/styles/**/*.less',
'<%= app %>/**/components/**/*.less'
],
tasks: ['less']
},

103
tasks/todos.js Normal file
View file

@ -0,0 +1,103 @@
module.exports = function (grunt) {
var _ = require('lodash');
var Promise = require('bluebird');
var readFileAsync = Promise.promisify(require('fs').readFile);
var spawnAsync = Promise.promisify(grunt.util.spawn);
var path = require('path');
var absolute = _.partial(path.join, path.join(__dirname, '..'));
var TODO_RE = /[\s\/\*]+(TODO|FIXME):?\s*(.+)/;
var NEWLINE_RE = /\r?\n/;
var TODO_FILENAME = 'TODOS.md';
var TYPE_PRIORITIES = {
'FIXME': 1
};
grunt.registerTask('todos', function () {
var files = grunt.file.expand([
'src/kibana/**/*.js',
'test/unit/specs/**/*.js'
]);
var matches = [];
var currentFile = null;
if (grunt.file.exists(TODO_FILENAME)) {
currentFile = grunt.file.read(TODO_FILENAME);
}
Promise.map(files, function (path) {
// grunt passes back these file names relative to the root... not
// what we want when we are calling fs.readFile
var absPath = absolute(path);
return readFileAsync(absPath, 'utf8')
.then(function (file) {
return file.split(NEWLINE_RE);
})
.each(function (line, i) {
var match = line.match(TODO_RE);
if (!match) return;
matches.push({
type: match[1],
msg: match[2],
path: path,
line: i + 1
});
});
}, { concurrency: 50 })
.then(function () {
var newFileLines = [
'# TODO items',
'> Automatically extracted',
''
];
var groupedByPath = _.groupBy(matches, 'path');
Object.keys(groupedByPath)
.sort(function (a, b) {
var aChunks = a.split(path.sep);
var bChunks = b.split(path.sep);
// compare the paths chunk by chunk
for (var i = 0; i < aChunks.length; i++) {
if (aChunks[i] === bChunks[i]) continue;
return aChunks[i].localeCompare(bChunks[i] || '');
}
})
.forEach(function (path) {
newFileLines.push(' - **' + path + '**');
_(groupedByPath[path])
.sortBy(function (match) {
return TYPE_PRIORITIES[match.type] || 0;
})
.each(function (match) {
var priority = TYPE_PRIORITIES[match.type] || 0;
newFileLines.push(
' - ' + (priority ? match.type + ' ' : '') +
match.msg + ' ' +
'[L' + match.line + ']' +
'(https://github.com/elasticsearch/kibana4/blob/master/' + match.path + '#L' + match.line + ')'
);
});
});
var newFile = newFileLines.join('\n');
if (newFile !== currentFile) {
grunt.log.ok('Committing updated TODO.md');
grunt.file.write(TODO_FILENAME, newFile);
return spawnAsync({
cmd: 'git',
args: ['add', absolute(TODO_FILENAME)]
});
} else {
grunt.log.ok('No changes to commit to TODO.md');
}
})
.nodeify(this.async());
});
};

View file

@ -1,86 +1,132 @@
define(function (require) {
var angular = require('angular');
var _ = require('lodash');
var sinon = require('sinon/sinon');
var sinon = require('test_utils/auto_release_sinon');
require('services/private');
// Load kibana
require('index');
describe('State Management', function () {
describe('Events', function () {
var $rootScope;
var Events;
describe('Events', function () {
require('test_utils/no_digest_promises').activateForSuite();
beforeEach(function () {
module('kibana');
var $rootScope;
var Events;
var Notifier;
var Promise;
inject(function (_$rootScope_, Private) {
$rootScope = _$rootScope_;
Events = Private(require('factories/events'));
});
beforeEach(function () {
module('kibana');
inject(function ($injector, Private) {
$rootScope = $injector.get('$rootScope');
Notifier = $injector.get('Notifier');
Promise = $injector.get('Promise');
Events = Private(require('factories/events'));
});
});
it('should handle on events', function (done) {
var obj = new Events();
obj.on('test', function (message) {
expect(message).to.equal('Hello World');
done();
});
obj.emit('test', 'Hello World');
$rootScope.$apply();
it('should handle on events', function (done) {
var obj = new Events();
obj.on('test', function (message) {
expect(message).to.equal('Hello World');
done();
});
obj.emit('test', 'Hello World');
});
it('should work with inherited objects', function (done) {
_.inherits(MyEventedObject, Events);
function MyEventedObject() {
MyEventedObject.Super.call(this);
}
var obj = new MyEventedObject();
obj.on('test', function (message) {
expect(message).to.equal('Hello World');
done();
});
obj.emit('test', 'Hello World');
$rootScope.$apply();
it('should work with inherited objects', function (done) {
_.inherits(MyEventedObject, Events);
function MyEventedObject() {
MyEventedObject.Super.call(this);
}
var obj = new MyEventedObject();
obj.on('test', function (message) {
expect(message).to.equal('Hello World');
done();
});
obj.emit('test', 'Hello World');
});
it('should clear events when off is called', function () {
var obj = new Events();
obj.on('test', _.noop);
expect(obj._listeners).to.have.property('test');
expect(obj._listeners['test']).to.have.length(1);
obj.off();
expect(obj._listeners).to.not.have.property('test');
});
it('should clear events when off is called', function () {
var obj = new Events();
obj.on('test', _.noop);
expect(obj._listeners).to.have.property('test');
expect(obj._listeners['test']).to.have.length(1);
obj.off();
expect(obj._listeners).to.not.have.property('test');
});
it('should clear a specific handler when off is called for an event', function () {
var obj = new Events();
var handler1 = sinon.stub();
var handler2 = sinon.stub();
obj.on('test', handler1);
obj.on('test', handler2);
expect(obj._listeners).to.have.property('test');
obj.off('test', handler1);
obj.emit('test', 'Hello World');
$rootScope.$apply();
it('should clear a specific handler when off is called for an event', function (done) {
var obj = new Events();
var handler1 = sinon.stub();
var handler2 = sinon.stub();
obj.on('test', handler1);
obj.on('test', handler2);
expect(obj._listeners).to.have.property('test');
obj.off('test', handler1);
obj.emit('test', 'Hello World').then(function () {
sinon.assert.calledOnce(handler2);
sinon.assert.notCalled(handler1);
done();
});
it('should clear a all handlers when off is called for an event', function () {
var obj = new Events();
var handler1 = sinon.stub();
obj.on('test', handler1);
expect(obj._listeners).to.have.property('test');
obj.off('test');
expect(obj._listeners).to.not.have.property('test');
obj.emit('test', 'Hello World');
$rootScope.$apply();
sinon.assert.notCalled(handler1);
});
});
});
it('should clear a all handlers when off is called for an event', function (done) {
var obj = new Events();
var handler1 = sinon.stub();
obj.on('test', handler1);
expect(obj._listeners).to.have.property('test');
obj.off('test');
expect(obj._listeners).to.not.have.property('test');
obj.emit('test', 'Hello World').then(function () {
sinon.assert.notCalled(handler1);
done();
});
});
it('should handle mulitple identical emits in the same tick', function (done) {
var obj = new Events();
var handler1 = sinon.stub();
obj.on('test', handler1);
var emits = [
obj.emit('test', 'one'),
obj.emit('test', 'two'),
obj.emit('test', 'three')
];
Promise.all(emits).then(function () {
expect(handler1.callCount).to.be(3);
expect(handler1.getCall(0).calledWith('one')).to.be(true);
expect(handler1.getCall(1).calledWith('two')).to.be(true);
expect(handler1.getCall(2).calledWith('three')).to.be(true);
done();
});
});
it('should handle emits from the handler', function (done) {
var obj = new Events();
var secondEmit = Promise.defer();
var handler1 = sinon.spy(function () {
if (handler1.calledTwice) {
return;
}
obj.emit('test').then(_.bindKey(secondEmit, 'resolve'));
});
obj.on('test', handler1);
Promise.all([
obj.emit('test'),
secondEmit.promise
]).then(function () {
expect(handler1.callCount).to.be(2);
done();
});
});
it('should only emit to handlers registered before emit is called');
});
});