From 5ba8b13ccfc5fec122e4f26fc492f32619dbfe2f Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sat, 30 Jan 2016 23:03:09 +0100 Subject: [PATCH] Add tests and documentation of ol.events.EventTarget --- src/ol/events/eventtarget.js | 31 ++++- test/spec/ol/events/eventtarget.test.js | 158 ++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 test/spec/ol/events/eventtarget.test.js diff --git a/src/ol/events/eventtarget.js b/src/ol/events/eventtarget.js index aeb772ac33..8b8ed80d15 100644 --- a/src/ol/events/eventtarget.js +++ b/src/ol/events/eventtarget.js @@ -5,6 +5,25 @@ goog.require('ol.events'); goog.require('ol.events.Event'); /** + * @classdesc + * A simplified implementation of the W3C DOM Level 2 EventTarget interface. + * @see {@link https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget} + * + * There are two important simplifications compared to the specification: + * + * 1. The handling of `useCapture` in `addEventListener` and + * `removeEventListener`. There is no real capture model. Instead, when + * adding a listener, `useCapture` means that it will be added as first + * listener, causing it to be called before other listeners. When removing a + * listener, the `useCapture` argument will be ignored, and the listener will + * be removed regardless of whether it was added with `useCapture` set to + * true or false. + * 2. The handling of `stopPropagation` and `preventDefault` on `dispatchEvent`. + * There is no event target hierarchy. When a listener calls + * `stopPropagation` or `preventDefault` on an event object, it means that no + * more listeners after this one will be called. Same as when the listener + * returns false. + * * @constructor * @extends {goog.Disposable} */ @@ -28,7 +47,8 @@ goog.inherits(ol.events.EventTarget, goog.Disposable); * @param {boolean=} opt_capture Call listener before already registered * listeners. Default is false. */ -ol.events.EventTarget.prototype.addEventListener = function(type, listener, opt_capture) { +ol.events.EventTarget.prototype.addEventListener = function( + type, listener, opt_capture) { var listeners = this.listeners_[type]; if (!listeners) { listeners = this.listeners_[type] = []; @@ -76,6 +96,9 @@ ol.events.EventTarget.prototype.disposeInternal = function() { /** + * Get the listeners for a specified event type. Listeners are returned in the + * opposite order that they will be called in. + * * @param {ol.events.EventType|string} type Type. * @return {Array.} Listeners. */ @@ -99,10 +122,10 @@ ol.events.EventTarget.prototype.hasListener = function(opt_type) { /** * @param {ol.events.EventType|string} type Type. * @param {ol.events.ListenerFunctionType} listener Listener. - * @param {boolean=} opt_capture Call listener before already registered - * listeners. Default is false. + * @param {boolean=} opt_capture Ignored. For W3C compatibility only. */ -ol.events.EventTarget.prototype.removeEventListener = function(type, listener, opt_capture) { +ol.events.EventTarget.prototype.removeEventListener = function( + type, listener, opt_capture) { var listeners = this.listeners_[type]; if (listeners) { var index = listeners.indexOf(listener); diff --git a/test/spec/ol/events/eventtarget.test.js b/test/spec/ol/events/eventtarget.test.js new file mode 100644 index 0000000000..5639cee17e --- /dev/null +++ b/test/spec/ol/events/eventtarget.test.js @@ -0,0 +1,158 @@ +goog.provide('ol.test.events.EventTarget'); + + +describe('ol.events.EventTarget', function() { + var called, events, eventTarget, spy1, spy2, spy3; + + beforeEach(function() { + called = []; + events = []; + function spy(evt) { + called.push(this.id); + events.push(evt); + } + spy1 = spy.bind({id: 1}); + spy2 = spy.bind({id: 2}); + spy3 = spy.bind({id: 3}); + eventTarget = new ol.events.EventTarget(); + }); + + describe('constructor', function() { + it('creates an instance', function() { + expect(eventTarget).to.be.a(ol.events.EventTarget); + }); + it('creates an empty listeners_ object', function() { + expect(Object.keys(eventTarget.listeners_)).to.have.length(0); + }); + }); + + describe('#hasListener', function() { + it('reports any listeners when called without argument', function() { + expect(eventTarget.hasListener()).to.be(false); + eventTarget.listeners_['foo'] = [function() {}]; + expect(eventTarget.hasListener()).to.be(true); + }); + it('reports listeners for the type passed as argument', function() { + eventTarget.listeners_['foo'] = [function() {}]; + expect(eventTarget.hasListener('foo')).to.be(true); + expect(eventTarget.hasListener('bar')).to.be(false); + }); + }); + + describe('#getListeners', function() { + it('returns listeners for a type or undefined if none', function() { + expect(eventTarget.getListeners('foo')).to.be(undefined); + var listeners = [function() {}]; + eventTarget.listeners_['foo'] = listeners; + expect(eventTarget.getListeners('foo')).to.equal(listeners); + }); + }); + + + describe('#addEventListener()', function() { + it('has listeners for each registered type', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.addEventListener('bar', spy2, false); + expect(eventTarget.hasListener('foo')).to.be(true); + expect(eventTarget.hasListener('bar')).to.be(true); + }); + it('registers listeners in the order determined by useCapture', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.addEventListener('foo', spy2, false); + eventTarget.addEventListener('foo', spy3, true); + expect(eventTarget.getListeners('foo')).to.eql([spy2, spy1, spy3]); + }); + it('does not re-add existing listeners, ignoring useCapture', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.addEventListener('foo', spy2, false); + eventTarget.addEventListener('foo', spy3, true); + eventTarget.addEventListener('foo', spy2); + eventTarget.addEventListener('foo', spy1, true); + eventTarget.addEventListener('foo', spy3, false); + expect(eventTarget.getListeners('foo')).to.eql([spy2, spy1, spy3]); + }); + }); + + describe('#removeEventListener()', function() { + it('keeps the listeners registry clean', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.removeEventListener('foo', spy1, false); + expect(eventTarget.hasListener('foo')).to.be(false); + }); + it('removes added listeners from the listeners registry', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.addEventListener('foo', spy2, false); + eventTarget.removeEventListener('foo', spy1, false); + expect(eventTarget.getListeners('foo')).to.have.length(1); + }); + it('ignores the useCapture setting when removing listeners', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.addEventListener('foo', spy2, false); + eventTarget.addEventListener('foo', spy3, true); + eventTarget.removeEventListener('foo', spy1, true); + eventTarget.removeEventListener('foo', spy2); + eventTarget.removeEventListener('foo', spy3, false); + expect(eventTarget.getListeners('foo')).to.be(undefined); + }); + }); + + describe('#dispatchEvent()', function() { + it('calls listeners in the correct order', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.addEventListener('foo', spy2, false); + eventTarget.addEventListener('foo', spy3, true); + eventTarget.dispatchEvent('foo'); + expect(called).to.eql([3, 1, 2]); + }); + it('stops propagation when listeners return false', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.addEventListener('foo', function(evt) { + spy2(); + return false; + }, false); + eventTarget.addEventListener('foo', spy3, false); + eventTarget.dispatchEvent('foo'); + expect(called).to.eql([1, 2]); + }); + it('stops propagation when listeners call preventDefault()', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.addEventListener('foo', function(evt) { + spy2(); + evt.preventDefault(); + }, true); + eventTarget.addEventListener('foo', spy3, false); + eventTarget.dispatchEvent('foo'); + expect(called).to.eql([2]); + }); + it('passes a default ol.events.Event object to listeners', function() { + eventTarget.addEventListener('foo', spy1, false); + eventTarget.dispatchEvent('foo'); + expect(events[0]).to.be.a(ol.events.Event); + expect(events[0].type).to.be('foo'); + expect(events[0].target).to.equal(eventTarget); + }); + it('passes a custom event object with target to listeners', function() { + eventTarget.addEventListener('foo', spy1, false); + var event = { + type: 'foo' + }; + eventTarget.dispatchEvent(event); + expect(events[0]).to.equal(event); + expect(events[0].target).to.equal(eventTarget); + }); + }); + + describe('#dispose()', function() { + it('cleans up foreign references', function() { + ol.events.listen(eventTarget, 'foo', spy1, false, document); + expect(eventTarget.hasListener('foo')).to.be(true); + eventTarget.dispose(); + expect(eventTarget.hasListener('foo')).to.be(false); + }); + }); +}); + + +goog.require('ol.events'); +goog.require('ol.events.Event'); +goog.require('ol.events.EventTarget');