Skip to content

Commit

Permalink
[js] Convert webdriver.EventEmitter to a native node module
Browse files Browse the repository at this point in the history
  • Loading branch information
jleyba committed Jan 25, 2016
1 parent 7a76c82 commit e4e7c53
Show file tree
Hide file tree
Showing 2 changed files with 387 additions and 0 deletions.
210 changes: 210 additions & 0 deletions javascript/node/selenium-webdriver/lib/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "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 WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

'use strict';

/**
* Describes an event listener registered on an {@linkplain EventEmitter}.
*/
class Listener {
/**
* @param {!Function} fn The acutal listener function.
* @param {(Object|undefined)} scope The object in whose scope to invoke the
* listener.
* @param {boolean} oneshot Whether this listener should only be used once.
*/
constructor(fn, scope, oneshot) {
Object.defineProperties(this, {
fn: {value: fn},
scope: {value: scope},
oneshot: {value: oneshot}
});
}
}


/** @type {!WeakMap<!EventEmitter, !Map<string, !Set<!Listener>>>} */
const EVENTS = new WeakMap;


/**
* Object that can emit events for others to listen for.
*/
class EventEmitter {
/**
* Fires an event and calls all listeners.
* @param {string} type The type of event to emit.
* @param {...*} var_args Any arguments to pass to each listener.
*/
emit(type, var_args) {
let events = EVENTS.get(this);
if (!events) {
return;
}

let args = Array.prototype.slice.call(arguments, 1);

let listeners = events.get(type);
if (listeners) {
for (let listener of listeners) {
listener.fn.apply(listener.scope, args);
if (listener.oneshot) {
listeners.delete(listener);
}
}
}
}

/**
* Returns a mutable list of listeners for a specific type of event.
* @param {string} type The type of event to retrieve the listeners for.
* @return {!Set<!Listener>} The registered listeners for the given event
* type.
*/
listeners(type) {
let events = EVENTS.get(this);
if (!events) {
events = new Map;
EVENTS.set(this, events);
}

let listeners = events.get(type);
if (!listeners) {
listeners = new Set;
events.set(type, listeners);
}
return listeners;
}

/**
* Registers a listener.
* @param {string} type The type of event to listen for.
* @param {!Function} fn The function to invoke when the event is fired.
* @param {Object=} opt_self The object in whose scope to invoke the listener.
* @param {boolean=} opt_oneshot Whether the listener should b (e removed after
* the first event is fired.
* @return {!EventEmitter} A self reference.
* @private
*/
addListener_(type, fn, opt_self, opt_oneshot) {
let listeners = this.listeners(type);
for (let listener of listeners) {
if (listener.fn === fn) {
return this;
}
}
listeners.add(new Listener(fn, opt_self || undefined, !!opt_oneshot));
return this;
}

/**
* Registers a listener.
* @param {string} type The type of event to listen for.
* @param {!Function} fn The function to invoke when the event is fired.
* @param {Object=} opt_self The object in whose scope to invoke the listener.
* @return {!EventEmitter} A self reference.
*/
addListener(type, fn, opt_self) {
return this.addListener_(type, fn, opt_self, false);
}

/**
* Registers a one-time listener which will be called only the first time an
* event is emitted, after which it will be removed.
* @param {string} type The type of event to listen for.
* @param {!Function} fn The function to invoke when the event is fired.
* @param {Object=} opt_self The object in whose scope to invoke the listener.
* @return {!EventEmitter} A self reference.
*/
once(type, fn, opt_self) {
return this.addListener_(type, fn, opt_self, true);
}

/**
* An alias for {@link #addListener() addListener()}.
* @param {string} type The type of event to listen for.
* @param {!Function} fn The function to invoke when the event is fired.
* @param {Object=} opt_self The object in whose scope to invoke the listener.
* @return {!EventEmitter} A self reference.
*/
on(type, fn, opt_self) {
return this.addListener(type, fn, opt_self);
}

/**
* Removes a previously registered event listener.
* @param {string} type The type of event to unregister.
* @param {!Function} listenerFn The handler function to remove.
* @return {!EventEmitter} A self reference.
*/
removeListener(type, listenerFn) {
if (typeof type !== 'string' || typeof listenerFn !== 'function') {
throw TypeError('invalid args: expected (string, function), got ('
+ (typeof type) + ', ' + (typeof listenerFn) + ')');
}

let events = EVENTS.get(this);
if (!events) {
return this;
}

let listeners = events.get(type);
if (!listeners) {
return this;
}

let match;
for (let listener of listeners) {
if (listener.fn === listenerFn) {
match = listener;
break;
}
}
if (match) {
listeners.delete(match);
if (!listeners.size) {
events.delete(type);
}
}
return this;
}

/**
* Removes all listeners for a specific type of event. If no event is
* specified, all listeners across all types will be removed.
* @param {string=} opt_type The type of event to remove listeners from.
* @return {!EventEmitter} A self reference.
*/
removeAllListeners(opt_type) {
let events = EVENTS.get(this);
if (events) {
if (typeof opt_type === 'string') {
events.delete(opt_type);
} else {
EVENTS.delete(this);
}
}
return this;
}
}


// PUBLIC API


exports.EventEmitter = EventEmitter;
exports.Listener = Listener;
177 changes: 177 additions & 0 deletions javascript/node/selenium-webdriver/test/lib/events_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "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 WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

'use strict';

const EventEmitter = require('../../lib/events').EventEmitter;

const assert = require('assert');
const sinon = require('sinon');

describe('EventEmitter', function() {
describe('#emit()', function() {
it('can emit events when nothing is registered', function() {
let emitter = new EventEmitter;
emitter.emit('foo');
// Ok if no errors are thrown.
});

it('can pass args to listeners on emit', function() {
let emitter = new EventEmitter;
let now = Date.now();

let messages = [];
emitter.on('foo', (arg) => messages.push(arg));

emitter.emit('foo', now);
assert.deepEqual([now], messages);

emitter.emit('foo', now + 15);
assert.deepEqual([now, now + 15], messages);
});
});

describe('#addListener()', function() {
it('can add multiple listeners for one event', function() {
let emitter = new EventEmitter;
let count = 0;
emitter.addListener('foo', () => count++);
emitter.addListener('foo', () => count++);
emitter.addListener('foo', () => count++);
emitter.emit('foo');
assert.equal(3, count);
});

it('only registers each listener function once', function() {
let emitter = new EventEmitter;
let count = 0;
let onFoo = () => count++;
emitter.addListener('foo', onFoo);
emitter.addListener('foo', onFoo);
emitter.addListener('foo', onFoo);

emitter.emit('foo');
assert.equal(1, count);

emitter.emit('foo');
assert.equal(2, count);
});

it('allows users to specify a custom scope', function() {
let obj = {
count: 0,
inc: function() {
this.count++;
}
};
let emitter = new EventEmitter;
emitter.addListener('foo', obj.inc, obj);

emitter.emit('foo');
assert.equal(1, obj.count);

emitter.emit('foo');
assert.equal(2, obj.count);
});
});

describe('#once()', function() {
it('only calls registered callback once', function() {
let emitter = new EventEmitter;
let count = 0;
emitter.once('foo', () => count++);
emitter.once('foo', () => count++);
emitter.once('foo', () => count++);

emitter.emit('foo');
assert.equal(3, count);

emitter.emit('foo');
assert.equal(3, count);

emitter.emit('foo');
assert.equal(3, count);
});
});

describe('#removeListeners()', function() {
it('only removes the given listener function', function() {
let emitter = new EventEmitter;
let count = 0;
emitter.addListener('foo', () => count++);
emitter.addListener('foo', () => count++);

let toRemove = () => count++;
emitter.addListener('foo', toRemove);

emitter.emit('foo');
assert.equal(3, count);

emitter.removeListener('foo', toRemove);
emitter.emit('foo');
assert.equal(5, count);
});
});

describe('#removeAllListeners()', function() {
it('only removes listeners for type if specified', function() {
let emitter = new EventEmitter;
let count = 0;
emitter.addListener('foo', () => count++);
emitter.addListener('foo', () => count++);
emitter.addListener('foo', () => count++);
emitter.addListener('bar', () => count++);

emitter.emit('foo');
assert.equal(3, count);

emitter.removeAllListeners('foo');

emitter.emit('foo');
assert.equal(3, count);

emitter.emit('bar');
assert.equal(4, count);
});

it('removes absolutely all listeners if no type specified', function() {
let emitter = new EventEmitter;
let count = 0;
emitter.addListener('foo', () => count++);
emitter.addListener('bar', () => count++);
emitter.addListener('baz', () => count++);
emitter.addListener('baz', () => count++);

emitter.emit('foo');
assert.equal(1, count);

emitter.emit('baz');
assert.equal(3, count);

emitter.removeAllListeners();

emitter.emit('foo');
assert.equal(3, count);

emitter.emit('bar');
assert.equal(3, count);

emitter.emit('baz');
assert.equal(3, count);
});
});
});

0 comments on commit e4e7c53

Please sign in to comment.