diff --git a/.airtap.yml b/.airtap.yml new file mode 100644 index 0000000..4ef94fa --- /dev/null +++ b/.airtap.yml @@ -0,0 +1,15 @@ +sauce_connect: true +loopback: airtap.local +browsers: + - name: chrome + version: latest + - name: firefox + version: latest + - name: safari + version: 9..latest + # - name: iphone + # version: latest + - name: ie + version: 9..latest + - name: microsoftedge + version: 13..latest diff --git a/.travis.yml b/.travis.yml index 54d7e04..37c7813 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,15 @@ sudo: false language: node_js node_js: - - '0.10' + - stable + - '0.12' script: - npm test - - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then npm run test:browsers; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_NODE_VERSION}" = "stable" ]; then npm run test:browsers; fi +addons: + sauce_connect: true + hosts: + - airtap.local env: global: - secure: XcBiD8yReflut9q7leKsigDZ0mI3qTKH+QrNVY8DaqlomJOZw8aOrVuX9Jz12l86ZJ41nbxmKnRNkFzcVr9mbP9YaeTb3DpeOBWmvaoSfud9Wnc16VfXtc1FCcwDhSVcSiM3UtnrmFU5cH+Dw1LPh5PbfylYOS/nJxUvG0FFLqI= diff --git a/.zuul.yml b/.zuul.yml deleted file mode 100644 index 216b335..0000000 --- a/.zuul.yml +++ /dev/null @@ -1,13 +0,0 @@ -ui: mocha-qunit -concurrency: 1 -browsers: - - name: chrome - version: latest - - name: firefox - version: latest - - name: safari - version: 7..latest - - name: iphone - version: latest - - name: ie - version: 8..latest diff --git a/Readme.md b/Readme.md index 4fae922..f2f8c4c 100644 --- a/Readme.md +++ b/Readme.md @@ -1,21 +1,48 @@ -# Status: [Maintainer Needed](https://github.com/Gozala/events/issues/43) - # events [![Build Status](https://travis-ci.org/Gozala/events.png?branch=master)](https://travis-ci.org/Gozala/events) -Node's event emitter for all engines. +> Node's event emitter for all engines. + +This implements the Node.js [`events`](http://nodejs.org/api/events.html) module for environments that do not have it, like browsers. + +> `events` currently matches the **Node.js 10.1** API. + +Note that the `events` module uses ES5 features. If you need to support very old browsers like IE8, use a shim like [`es5-shim`](https://www.npmjs.com/package/es5-shim). You need both the shim and the sham versions of `es5-shim`. + +This module is maintained, but only by very few people. If you'd like to help, let us know in the [Maintainer Needed](https://github.com/Gozala/events/issues/43) issue! + +## Install + +You usually do not have to install `events` yourself! If your code runs in Node.js, `events` is built in. If your code runs in the browser, bundlers like [browserify](https://github.com/browserify/browserify) or [webpack](https://github.com/webpack/webpack) also include the `events` module. -## Install ## +But if none of those apply, with npm do: ``` npm install events ``` -## Require ## +## Usage ```javascript var EventEmitter = require('events') + +var ee = new EventEmitter() +ee.on('message', function (text) { + console.log(text) +}) +ee.emit('message', 'hello world') ``` -## Usage ## +## API + +See the [Node.js EventEmitter docs](http://nodejs.org/api/events.html). `events` currently matches the Node.js 10.1 API. + +## Contributing + +PRs are very welcome! The main way to contribute to `events` is by porting features, bugfixes and tests from Node.js. Ideally, code contributions to this module are copy-pasted from Node.js and transpiled to ES5, rather than reimplemented from scratch. Matching the Node.js code as closely as possible makes maintenance simpler when new changes land in Node.js. +This module intends to provide exactly the same API as Node.js, so features that are not available in the core `events` module will not be accepted. Feature requests should instead be directed at [nodejs/node](https://github.com/nodejs/node) and will be added to this module once they are implemented in Node.js. + +If there is a difference in behaviour between Node.js's `events` module and this module, please open an issue! + +## License -See the [node.js event emitter docs](http://nodejs.org/api/events.html) +[MIT](./LICENSE) diff --git a/events.js b/events.js index a17bcba..6dbc6eb 100644 --- a/events.js +++ b/events.js @@ -19,17 +19,39 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -var objectCreate = Object.create || objectCreatePolyfill -var objectKeys = Object.keys || objectKeysPolyfill -var bind = Function.prototype.bind || functionBindPolyfill +'use strict'; -function EventEmitter() { - if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) { - this._events = objectCreate(null); - this._eventsCount = 0; +var R = typeof Reflect === 'object' ? Reflect : null +var ReflectApply = R && typeof R.apply === 'function' + ? R.apply + : function ReflectApply(target, receiver, args) { + return Function.prototype.apply.call(target, receiver, args); } - this._maxListeners = this._maxListeners || undefined; +var ReflectOwnKeys +if (R && typeof R.ownKeys === 'function') { + ReflectOwnKeys = R.ownKeys +} else if (Object.getOwnPropertySymbols) { + ReflectOwnKeys = function ReflectOwnKeys(target) { + return Object.getOwnPropertyNames(target) + .concat(Object.getOwnPropertySymbols(target)); + }; +} else { + ReflectOwnKeys = function ReflectOwnKeys(target) { + return Object.getOwnPropertyNames(target); + }; +} + +function ProcessEmitWarning(warning) { + if (console && console.warn) console.warn(warning); +} + +var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) { + return value !== value; +} + +function EventEmitter() { + EventEmitter.init.call(this); } module.exports = EventEmitter; @@ -37,41 +59,43 @@ module.exports = EventEmitter; EventEmitter.EventEmitter = EventEmitter; EventEmitter.prototype._events = undefined; +EventEmitter.prototype._eventsCount = 0; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. var defaultMaxListeners = 10; -var hasDefineProperty; -try { - var o = {}; - if (Object.defineProperty) Object.defineProperty(o, 'x', { value: 0 }); - hasDefineProperty = o.x === 0; -} catch (err) { hasDefineProperty = false } -if (hasDefineProperty) { - Object.defineProperty(EventEmitter, 'defaultMaxListeners', { - enumerable: true, - get: function() { - return defaultMaxListeners; - }, - set: function(arg) { - // check whether the input is a positive number (whose value is zero or - // greater and not a NaN). - if (typeof arg !== 'number' || arg < 0 || arg !== arg) - throw new TypeError('"defaultMaxListeners" must be a positive number'); - defaultMaxListeners = arg; +Object.defineProperty(EventEmitter, 'defaultMaxListeners', { + enumerable: true, + get: function() { + return defaultMaxListeners; + }, + set: function(arg) { + if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { + throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.'); } - }); -} else { - EventEmitter.defaultMaxListeners = defaultMaxListeners; -} + defaultMaxListeners = arg; + } +}); + +EventEmitter.init = function() { + + if (this._events === undefined || + this._events === Object.getPrototypeOf(this)._events) { + this._events = Object.create(null); + this._eventsCount = 0; + } + + this._maxListeners = this._maxListeners || undefined; +}; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { - if (typeof n !== 'number' || n < 0 || isNaN(n)) - throw new TypeError('"n" argument must be a positive number'); + if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { + throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.'); + } this._maxListeners = n; return this; }; @@ -86,115 +110,45 @@ EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return $getMaxListeners(this); }; -// These standalone emit* functions are used to optimize calling of event -// handlers for fast cases because emit() itself often has a variable number of -// arguments and can be deoptimized because of that. These functions always have -// the same number of arguments and thus do not get deoptimized, so the code -// inside them can execute faster. -function emitNone(handler, isFn, self) { - if (isFn) - handler.call(self); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self); - } -} -function emitOne(handler, isFn, self, arg1) { - if (isFn) - handler.call(self, arg1); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1); - } -} -function emitTwo(handler, isFn, self, arg1, arg2) { - if (isFn) - handler.call(self, arg1, arg2); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1, arg2); - } -} -function emitThree(handler, isFn, self, arg1, arg2, arg3) { - if (isFn) - handler.call(self, arg1, arg2, arg3); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1, arg2, arg3); - } -} - -function emitMany(handler, isFn, self, args) { - if (isFn) - handler.apply(self, args); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].apply(self, args); - } -} - EventEmitter.prototype.emit = function emit(type) { - var er, handler, len, args, i, events; + var args = []; + for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); var doError = (type === 'error'); - events = this._events; - if (events) - doError = (doError && events.error == null); + var events = this._events; + if (events !== undefined) + doError = (doError && events.error === undefined); else if (!doError) return false; // If there is no 'error' event listener then throw. if (doError) { - if (arguments.length > 1) - er = arguments[1]; + var er; + if (args.length > 0) + er = args[0]; if (er instanceof Error) { + // Note: The comments on the `throw` lines are intentional, they show + // up in Node's output if this results in an unhandled exception. throw er; // Unhandled 'error' event - } else { - // At least give some kind of context to the user - var err = new Error('Unhandled "error" event. (' + er + ')'); - err.context = er; - throw err; } - return false; + // At least give some kind of context to the user + var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : '')); + err.context = er; + throw err; // Unhandled 'error' event } - handler = events[type]; + var handler = events[type]; - if (!handler) + if (handler === undefined) return false; - var isFn = typeof handler === 'function'; - len = arguments.length; - switch (len) { - // fast cases - case 1: - emitNone(handler, isFn, this); - break; - case 2: - emitOne(handler, isFn, this, arguments[1]); - break; - case 3: - emitTwo(handler, isFn, this, arguments[1], arguments[2]); - break; - case 4: - emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); - break; - // slower - default: - args = new Array(len - 1); - for (i = 1; i < len; i++) - args[i - 1] = arguments[i]; - emitMany(handler, isFn, this, args); + if (typeof handler === 'function') { + ReflectApply(handler, this, args); + } else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + ReflectApply(listeners[i], this, args); } return true; @@ -205,19 +159,20 @@ function _addListener(target, type, listener, prepend) { var events; var existing; - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); + if (typeof listener !== 'function') { + throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); + } events = target._events; - if (!events) { - events = target._events = objectCreate(null); + if (events === undefined) { + events = target._events = Object.create(null); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". - if (events.newListener) { + if (events.newListener !== undefined) { target.emit('newListener', type, - listener.listener ? listener.listener : listener); + listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object @@ -226,7 +181,7 @@ function _addListener(target, type, listener, prepend) { existing = events[type]; } - if (!existing) { + if (existing === undefined) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; @@ -234,33 +189,29 @@ function _addListener(target, type, listener, prepend) { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = - prepend ? [listener, existing] : [existing, listener]; - } else { + prepend ? [listener, existing] : [existing, listener]; // If we've already got an array, just append. - if (prepend) { - existing.unshift(listener); - } else { - existing.push(listener); - } + } else if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); } // Check for listener leak - if (!existing.warned) { - m = $getMaxListeners(target); - if (m && m > 0 && existing.length > m) { - existing.warned = true; - var w = new Error('Possible EventEmitter memory leak detected. ' + - existing.length + ' "' + String(type) + '" listeners ' + - 'added. Use emitter.setMaxListeners() to ' + - 'increase limit.'); - w.name = 'MaxListenersExceededWarning'; - w.emitter = target; - w.type = type; - w.count = existing.length; - if (typeof console === 'object' && console.warn) { - console.warn('%s: %s', w.name, w.message); - } - } + m = $getMaxListeners(target); + if (m > 0 && existing.length > m && !existing.warned) { + existing.warned = true; + // No error code for this since it is a Warning + // eslint-disable-next-line no-restricted-syntax + var w = new Error('Possible EventEmitter memory leak detected. ' + + existing.length + ' ' + String(type) + ' listeners ' + + 'added. Use emitter.setMaxListeners() to ' + + 'increase limit'); + w.name = 'MaxListenersExceededWarning'; + w.emitter = target; + w.type = type; + w.count = existing.length; + ProcessEmitWarning(w); } } @@ -279,47 +230,36 @@ EventEmitter.prototype.prependListener = }; function onceWrapper() { + var args = []; + for (var i = 0; i < arguments.length; i++) args.push(arguments[i]); if (!this.fired) { this.target.removeListener(this.type, this.wrapFn); this.fired = true; - switch (arguments.length) { - case 0: - return this.listener.call(this.target); - case 1: - return this.listener.call(this.target, arguments[0]); - case 2: - return this.listener.call(this.target, arguments[0], arguments[1]); - case 3: - return this.listener.call(this.target, arguments[0], arguments[1], - arguments[2]); - default: - var args = new Array(arguments.length); - for (var i = 0; i < args.length; ++i) - args[i] = arguments[i]; - this.listener.apply(this.target, args); - } + ReflectApply(this.listener, this.target, args); } } function _onceWrap(target, type, listener) { var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; - var wrapped = bind.call(onceWrapper, state); + var wrapped = onceWrapper.bind(state); wrapped.listener = listener; state.wrapFn = wrapped; return wrapped; } EventEmitter.prototype.once = function once(type, listener) { - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); + if (typeof listener !== 'function') { + throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); + } this.on(type, _onceWrap(this, type, listener)); return this; }; EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); + if (typeof listener !== 'function') { + throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); + } this.prependListener(type, _onceWrap(this, type, listener)); return this; }; @@ -329,20 +269,21 @@ EventEmitter.prototype.removeListener = function removeListener(type, listener) { var list, events, position, i, originalListener; - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); + if (typeof listener !== 'function') { + throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); + } events = this._events; - if (!events) + if (events === undefined) return this; list = events[type]; - if (!list) + if (list === undefined) return this; if (list === listener || list.listener === listener) { if (--this._eventsCount === 0) - this._events = objectCreate(null); + this._events = Object.create(null); else { delete events[type]; if (events.removeListener) @@ -364,35 +305,38 @@ EventEmitter.prototype.removeListener = if (position === 0) list.shift(); - else + else { spliceOne(list, position); + } if (list.length === 1) events[type] = list[0]; - if (events.removeListener) + if (events.removeListener !== undefined) this.emit('removeListener', type, originalListener || listener); } return this; }; +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { var listeners, events, i; events = this._events; - if (!events) + if (events === undefined) return this; // not listening for removeListener, no need to emit - if (!events.removeListener) { + if (events.removeListener === undefined) { if (arguments.length === 0) { - this._events = objectCreate(null); + this._events = Object.create(null); this._eventsCount = 0; - } else if (events[type]) { + } else if (events[type] !== undefined) { if (--this._eventsCount === 0) - this._events = objectCreate(null); + this._events = Object.create(null); else delete events[type]; } @@ -401,7 +345,7 @@ EventEmitter.prototype.removeAllListeners = // emit removeListener for all listeners on all events if (arguments.length === 0) { - var keys = objectKeys(events); + var keys = Object.keys(events); var key; for (i = 0; i < keys.length; ++i) { key = keys[i]; @@ -409,7 +353,7 @@ EventEmitter.prototype.removeAllListeners = this.removeAllListeners(key); } this.removeAllListeners('removeListener'); - this._events = objectCreate(null); + this._events = Object.create(null); this._eventsCount = 0; return this; } @@ -418,7 +362,7 @@ EventEmitter.prototype.removeAllListeners = if (typeof listeners === 'function') { this.removeListener(type, listeners); - } else if (listeners) { + } else if (listeners !== undefined) { // LIFO order for (i = listeners.length - 1; i >= 0; i--) { this.removeListener(type, listeners[i]); @@ -431,17 +375,18 @@ EventEmitter.prototype.removeAllListeners = function _listeners(target, type, unwrap) { var events = target._events; - if (!events) + if (events === undefined) return []; var evlistener = events[type]; - if (!evlistener) + if (evlistener === undefined) return []; if (typeof evlistener === 'function') return unwrap ? [evlistener.listener || evlistener] : [evlistener]; - return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); + return unwrap ? + unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); } EventEmitter.prototype.listeners = function listeners(type) { @@ -464,12 +409,12 @@ EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { var events = this._events; - if (events) { + if (events !== undefined) { var evlistener = events[type]; if (typeof evlistener === 'function') { return 1; - } else if (evlistener) { + } else if (evlistener !== undefined) { return evlistener.length; } } @@ -478,16 +423,9 @@ function listenerCount(type) { } EventEmitter.prototype.eventNames = function eventNames() { - return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; + return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; }; -// About 1.5x faster than the two-arg version of Array#splice(). -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) - list[i] = list[k]; - list.pop(); -} - function arrayClone(arr, n) { var copy = new Array(n); for (var i = 0; i < n; ++i) @@ -495,6 +433,12 @@ function arrayClone(arr, n) { return copy; } +function spliceOne(list, index) { + for (; index + 1 < list.length; index++) + list[index] = list[index + 1]; + list.pop(); +} + function unwrapListeners(arr) { var ret = new Array(arr.length); for (var i = 0; i < ret.length; ++i) { @@ -502,22 +446,3 @@ function unwrapListeners(arr) { } return ret; } - -function objectCreatePolyfill(proto) { - var F = function() {}; - F.prototype = proto; - return new F; -} -function objectKeysPolyfill(obj) { - var keys = []; - for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) { - keys.push(k); - } - return k; -} -function functionBindPolyfill(context) { - var fn = this; - return function () { - return fn.apply(context, arguments); - }; -} diff --git a/package.json b/package.json index bbd8fd5..7509b48 100644 --- a/package.json +++ b/package.json @@ -20,17 +20,16 @@ }, "main": "./events.js", "engines": { - "node": ">=0.4.x" + "node": ">=0.8.x" }, "devDependencies": { + "airtap": "0.0.6", "isarray": "^2.0.2", - "mocha": "^3.5.3", - "object-keys": "^1.0.11", - "zuul": "^3.11.1" + "tape": "^4.8.0" }, "scripts": { - "test": "mocha --ui qunit -- tests/index.js", - "test:browsers": "zuul -- tests/index.js" + "test": "node tests/index.js", + "test:browsers": "airtap -- tests/index.js" }, "license": "MIT" } diff --git a/tests/add-listeners.js b/tests/add-listeners.js index 6682862..9b57827 100644 --- a/tests/add-listeners.js +++ b/tests/add-listeners.js @@ -108,4 +108,4 @@ assert.throws(function() { var ee = new EventEmitter(); ee.on('foo', null); -}, /^TypeError: "listener" argument must be a function$/); +}, /^TypeError: The "listener" argument must be of type Function. Received type object$/); diff --git a/tests/check-listener-leaks.js b/tests/check-listener-leaks.js index 3cd0aa2..7fce48f 100644 --- a/tests/check-listener-leaks.js +++ b/tests/check-listener-leaks.js @@ -23,6 +23,14 @@ var common = require('./common'); var assert = require('assert'); var events = require('../'); +// Redirect warning output to tape. +var consoleWarn = console.warn; +console.warn = common.test.comment; + +common.test.on('end', function () { + console.warn = consoleWarn; +}); + // default { var e = new events.EventEmitter(); diff --git a/tests/common.js b/tests/common.js index cdd5aea..49569b0 100644 --- a/tests/common.js +++ b/tests/common.js @@ -19,6 +19,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +var test = require('tape'); var assert = require('assert'); var noop = function() {}; @@ -77,7 +78,7 @@ function _mustCallInner(fn, criteria, field) { context[field] = criteria; // add the exit listener only once to avoid listener leak warnings - if (mustCallChecks.length === 0) after(function() { runCallChecks(0); }); + if (mustCallChecks.length === 0) test.onFinish(function() { runCallChecks(0); }); mustCallChecks.push(context); diff --git a/tests/errors.js b/tests/errors.js new file mode 100644 index 0000000..a23df43 --- /dev/null +++ b/tests/errors.js @@ -0,0 +1,13 @@ +'use strict'; +var assert = require('assert'); +var EventEmitter = require('../'); + +var EE = new EventEmitter(); + +assert.throws(function () { + EE.emit('error', 'Accepts a string'); +}, 'Error: Unhandled error. (Accepts a string)'); + +assert.throws(function () { + EE.emit('error', { message: 'Error!' }); +}, 'Unhandled error. ([object Object])'); diff --git a/tests/events-list.js b/tests/events-list.js new file mode 100644 index 0000000..08aa621 --- /dev/null +++ b/tests/events-list.js @@ -0,0 +1,28 @@ +'use strict'; + +var EventEmitter = require('../'); +var assert = require('assert'); + +var EE = new EventEmitter(); +var m = function() {}; +EE.on('foo', function() {}); +assert.equal(1, EE.eventNames().length); +assert.equal('foo', EE.eventNames()[0]); +EE.on('bar', m); +assert.equal(2, EE.eventNames().length); +assert.equal('foo', EE.eventNames()[0]); +assert.equal('bar', EE.eventNames()[1]); +EE.removeListener('bar', m); +assert.equal(1, EE.eventNames().length); +assert.equal('foo', EE.eventNames()[0]); + +if (typeof Symbol !== 'undefined') { + var s = Symbol('s'); + EE.on(s, m); + assert.equal(2, EE.eventNames().length); + assert.equal('foo', EE.eventNames()[0]); + assert.equal(s, EE.eventNames()[1]); + EE.removeListener(s, m); + assert.equal(1, EE.eventNames().length); + assert.equal('foo', EE.eventNames()[0]); +} diff --git a/tests/index.js b/tests/index.js index f71bc98..1f06256 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,25 +1,48 @@ +var test = require('tape'); require('./legacy-compat'); +var common = require('./common'); // we do this to easily wrap each file in a mocha test // and also have browserify be able to statically analyze this file var orig_require = require; var require = function(file) { - test(file, function() { - orig_require(file); + test(file, function(t) { + // Store the tape object so tests can access it. + t.on('end', function () { delete common.test; }); + common.test = t; + + try { orig_require(file); } catch (err) { t.fail(err); } + t.end(); }); }; require('./add-listeners.js'); require('./check-listener-leaks.js'); +require('./errors.js'); +require('./events-list.js'); require('./listener-count.js'); require('./listeners-side-effects.js'); require('./listeners.js'); require('./max-listeners.js'); +if ((function A () {}).name === 'A') { + require('./method-names.js'); +} else { + // Function.name is not supported in IE + test('./method-names.js', { skip: true }, function () {}); +} require('./modify-in-emit.js'); require('./num-args.js'); require('./once.js'); +require('./prepend.js'); require('./set-max-listeners-side-effects.js'); +require('./special-event-names.js'); require('./subclass.js'); +if (typeof Symbol === 'function') { + require('./symbols.js'); +} else { + // Symbol is not available. + test('./symbols.js', { skip: true }, function () {}); +} require('./remove-all-listeners.js'); require('./remove-listeners.js'); diff --git a/tests/listeners-side-effects.js b/tests/listeners-side-effects.js index fe223e3..180f833 100644 --- a/tests/listeners-side-effects.js +++ b/tests/listeners-side-effects.js @@ -23,7 +23,6 @@ require('./common'); var assert = require('assert'); var EventEmitter = require('../').EventEmitter; -var objectKeys = require('object-keys'); var e = new EventEmitter(); var fl; // foo listeners @@ -32,7 +31,7 @@ fl = e.listeners('foo'); assert.ok(Array.isArray(fl)); assert.strictEqual(fl.length, 0); if (Object.create) assert.ok(!(e._events instanceof Object)); -assert.strictEqual(objectKeys(e._events).length, 0); +assert.strictEqual(Object.keys(e._events).length, 0); e.on('foo', assert.fail); fl = e.listeners('foo'); diff --git a/tests/max-listeners.js b/tests/max-listeners.js index 162fb03..0b43953 100644 --- a/tests/max-listeners.js +++ b/tests/max-listeners.js @@ -33,8 +33,8 @@ e.on('maxListeners', common.mustCall()); e.setMaxListeners(42); var throwsObjs = [NaN, -1, 'and even this']; -var maxError = /^TypeError: "n" argument must be a positive number$/; -var defError = /^TypeError: "defaultMaxListeners" must be a positive number$/; +var maxError = /^RangeError: The value of "n" is out of range\. It must be a non-negative number\./; +var defError = /^RangeError: The value of "defaultMaxListeners" is out of range\. It must be a non-negative number\./; for (var i = 0; i < throwsObjs.length; i++) { var obj = throwsObjs[i]; diff --git a/tests/method-names.js b/tests/method-names.js new file mode 100644 index 0000000..364a161 --- /dev/null +++ b/tests/method-names.js @@ -0,0 +1,35 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('./common'); +var assert = require('assert'); +var events = require('../'); + +var E = events.EventEmitter.prototype; +assert.strictEqual(E.constructor.name, 'EventEmitter'); +assert.strictEqual(E.on, E.addListener); // Same method. +assert.strictEqual(E.off, E.removeListener); // Same method. +Object.getOwnPropertyNames(E).forEach(function(name) { + if (name === 'constructor' || name === 'on' || name === 'off') return; + if (typeof E[name] !== 'function') return; + assert.strictEqual(E[name].name, name); +}); diff --git a/tests/num-args.js b/tests/num-args.js index c5a085b..c9b0deb 100644 --- a/tests/num-args.js +++ b/tests/num-args.js @@ -23,13 +23,6 @@ require('./common'); var assert = require('assert'); var events = require('../'); -var after_checks = []; -after(function() { - for (var i = 0 ; i < after_checks.length ; ++i) { - after_checks[i](); - } -}); - var e = new events.EventEmitter(); var num_args_emitted = []; @@ -55,15 +48,13 @@ e.emit('numArgs', null, null, null, null, null); e.emit('foo', null, null, null, null); -after_checks.push(function() { - assert.ok(Array.isArray(num_args_emitted)); - assert.strictEqual(num_args_emitted.length, 8); - assert.strictEqual(num_args_emitted[0], 0); - assert.strictEqual(num_args_emitted[1], 1); - assert.strictEqual(num_args_emitted[2], 2); - assert.strictEqual(num_args_emitted[3], 3); - assert.strictEqual(num_args_emitted[4], 4); - assert.strictEqual(num_args_emitted[5], 5); - assert.strictEqual(num_args_emitted[6], 4); - assert.strictEqual(num_args_emitted[6], 4); -}); +assert.ok(Array.isArray(num_args_emitted)); +assert.strictEqual(num_args_emitted.length, 8); +assert.strictEqual(num_args_emitted[0], 0); +assert.strictEqual(num_args_emitted[1], 1); +assert.strictEqual(num_args_emitted[2], 2); +assert.strictEqual(num_args_emitted[3], 3); +assert.strictEqual(num_args_emitted[4], 4); +assert.strictEqual(num_args_emitted[5], 5); +assert.strictEqual(num_args_emitted[6], 4); +assert.strictEqual(num_args_emitted[6], 4); diff --git a/tests/once.js b/tests/once.js index 44c0bb0..4b36c05 100644 --- a/tests/once.js +++ b/tests/once.js @@ -53,7 +53,7 @@ assert.throws(function() { var ee = new EventEmitter(); ee.once('foo', null); -}, /^TypeError: "listener" argument must be a function$/); +}, /^TypeError: The "listener" argument must be of type Function. Received type object$/); { // once() has different code paths based on the number of arguments being diff --git a/tests/prepend.js b/tests/prepend.js new file mode 100644 index 0000000..79afde0 --- /dev/null +++ b/tests/prepend.js @@ -0,0 +1,31 @@ +'use strict'; + +var common = require('./common'); +var EventEmitter = require('../'); +var assert = require('assert'); + +var myEE = new EventEmitter(); +var m = 0; +// This one comes last. +myEE.on('foo', common.mustCall(function () { + assert.strictEqual(m, 2); +})); + +// This one comes second. +myEE.prependListener('foo', common.mustCall(function () { + assert.strictEqual(m++, 1); +})); + +// This one comes first. +myEE.prependOnceListener('foo', + common.mustCall(function () { + assert.strictEqual(m++, 0); + })); + +myEE.emit('foo'); + +// Verify that the listener must be a function +assert.throws(function () { + var ee = new EventEmitter(); + ee.prependOnceListener('foo', null); +}, 'TypeError: The "listener" argument must be of type Function. Received type object'); diff --git a/tests/remove-all-listeners.js b/tests/remove-all-listeners.js index df29998..622941c 100644 --- a/tests/remove-all-listeners.js +++ b/tests/remove-all-listeners.js @@ -22,17 +22,11 @@ var common = require('./common'); var assert = require('assert'); var events = require('../'); - -var after_checks = []; -after(function() { - for (var i = 0 ; i < after_checks.length ; ++i) { - after_checks[i](); - } -}); +var test = require('tape'); function expect(expected) { var actual = []; - after_checks.push(function() { + test.onFinish(function() { var sortedActual = actual.sort(); var sortedExpected = expected.sort(); assert.strictEqual(sortedActual.length, sortedExpected.length); diff --git a/tests/remove-listeners.js b/tests/remove-listeners.js index 6cccd96..18e4d16 100644 --- a/tests/remove-listeners.js +++ b/tests/remove-listeners.js @@ -176,7 +176,7 @@ assert.throws(function() { var ee = new EventEmitter(); ee.removeListener('foo', null); -}, /^TypeError: "listener" argument must be a function$/); +}, /^TypeError: The "listener" argument must be of type Function\. Received type object$/); { var ee = new EventEmitter(); diff --git a/tests/set-max-listeners-side-effects.js b/tests/set-max-listeners-side-effects.js index 99471db..13dbb67 100644 --- a/tests/set-max-listeners-side-effects.js +++ b/tests/set-max-listeners-side-effects.js @@ -22,11 +22,10 @@ require('./common'); var assert = require('assert'); var events = require('../'); -var objectKeys = require('object-keys'); var e = new events.EventEmitter(); if (Object.create) assert.ok(!(e._events instanceof Object)); -assert.strictEqual(objectKeys(e._events).length, 0); +assert.strictEqual(Object.keys(e._events).length, 0); e.setMaxListeners(5); -assert.strictEqual(objectKeys(e._events).length, 0); +assert.strictEqual(Object.keys(e._events).length, 0); diff --git a/tests/special-event-names.js b/tests/special-event-names.js new file mode 100644 index 0000000..a2f0b74 --- /dev/null +++ b/tests/special-event-names.js @@ -0,0 +1,45 @@ +'use strict'; + +var common = require('./common'); +var EventEmitter = require('../'); +var assert = require('assert'); + +var ee = new EventEmitter(); +var handler = function() {}; + +assert.strictEqual(ee.eventNames().length, 0); + +assert.strictEqual(ee._events.hasOwnProperty, undefined); +assert.strictEqual(ee._events.toString, undefined); + +ee.on('__defineGetter__', handler); +ee.on('toString', handler); +ee.on('__proto__', handler); + +assert.strictEqual(ee.eventNames()[0], '__defineGetter__'); +assert.strictEqual(ee.eventNames()[1], 'toString'); + +assert.strictEqual(ee.listeners('__defineGetter__').length, 1); +assert.strictEqual(ee.listeners('__defineGetter__')[0], handler); +assert.strictEqual(ee.listeners('toString').length, 1); +assert.strictEqual(ee.listeners('toString')[0], handler); + +// Only run __proto__ tests if that property can actually be set +if ({ __proto__: 'ok' }.__proto__ === 'ok') { + assert.strictEqual(ee.eventNames().length, 3); + assert.strictEqual(ee.eventNames()[2], '__proto__'); + assert.strictEqual(ee.listeners('__proto__').length, 1); + assert.strictEqual(ee.listeners('__proto__')[0], handler); + + ee.on('__proto__', common.mustCall(function(val) { + assert.strictEqual(val, 1); + })); + ee.emit('__proto__', 1); + + process.on('__proto__', common.mustCall(function(val) { + assert.strictEqual(val, 1); + })); + process.emit('__proto__', 1); +} else { + console.log('# skipped __proto__') +} diff --git a/tests/subclass.js b/tests/subclass.js index 6c2ae64..bd033ff 100644 --- a/tests/subclass.js +++ b/tests/subclass.js @@ -20,17 +20,10 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. var common = require('./common'); +var test = require('tape'); var assert = require('assert'); var EventEmitter = require('../').EventEmitter; var util = require('util'); -var objectKeys = require('object-keys'); - -var after_checks = []; -after(function() { - for (var i = 0 ; i < after_checks.length ; ++i) { - after_checks[i](); - } -}); util.inherits(MyEE, EventEmitter); @@ -53,9 +46,9 @@ assert.throws(function() { new ErrorEE(); }, /blerg/); -after_checks.push(function() { - if (Object.create) assert.ok(!(myee._events instanceof Object)); - assert.strictEqual(objectKeys(myee._events).length, 0); +test.onFinish(function() { + assert.ok(!(myee._events instanceof Object)); + assert.strictEqual(Object.keys(myee._events).length, 0); }); diff --git a/tests/symbols.js b/tests/symbols.js new file mode 100644 index 0000000..0721f0e --- /dev/null +++ b/tests/symbols.js @@ -0,0 +1,25 @@ +'use strict'; + +var common = require('./common'); +var EventEmitter = require('../'); +var assert = require('assert'); + +var ee = new EventEmitter(); +var foo = Symbol('foo'); +var listener = common.mustCall(); + +ee.on(foo, listener); +assert.strictEqual(ee.listeners(foo).length, 1); +assert.strictEqual(ee.listeners(foo)[0], listener); + +ee.emit(foo); + +ee.removeAllListeners(); +assert.strictEqual(ee.listeners(foo).length, 0); + +ee.on(foo, listener); +assert.strictEqual(ee.listeners(foo).length, 1); +assert.strictEqual(ee.listeners(foo)[0], listener); + +ee.removeListener(foo, listener); +assert.strictEqual(ee.listeners(foo).length, 0);