From c0f6fb770caa6faf74f5045303cb12d96036b795 Mon Sep 17 00:00:00 2001 From: Keegan Date: Fri, 5 Feb 2016 15:32:45 -0800 Subject: [PATCH] drop ClayEvents.one since it is a pain to work with and add tests for ClayEvents --- src/scripts/lib/clay-config.js | 11 +++ src/scripts/lib/clay-events.js | 72 ++++++++------- src/scripts/vendor/minified/minified.js | 71 ++++++++++++++- test/spec/lib/clay-config.js | 35 +++++++- test/spec/lib/clay-events.js | 114 ++++++++++++++++++++++++ test/spec/lib/clay-item.js | 1 - 6 files changed, 265 insertions(+), 39 deletions(-) create mode 100644 test/spec/lib/clay-events.js diff --git a/src/scripts/lib/clay-config.js b/src/scripts/lib/clay-config.js index 7badb62..b8c17b4 100644 --- a/src/scripts/lib/clay-config.js +++ b/src/scripts/lib/clay-config.js @@ -193,7 +193,18 @@ ClayConfig.registerComponent = function(component) { var _component = _.copyObj(component); if (typeof _component.manipulator === 'string') { _component.manipulator = manipulators[component.manipulator]; + + if (!_component.manipulator) { + throw new Error('The manipulator: ' + component.manipulator + + ' does not exist in the built-in manipulators.'); + } } + + if (typeof _component.manipulator.set !== 'function' || + typeof _component.manipulator.get !== 'function') { + throw new Error('The manipulator must have both a `get` and `set` method'); + } + componentStore[_component.name] = _component; }; diff --git a/src/scripts/lib/clay-events.js b/src/scripts/lib/clay-events.js index fc5378f..643544e 100644 --- a/src/scripts/lib/clay-events.js +++ b/src/scripts/lib/clay-events.js @@ -1,7 +1,7 @@ 'use strict'; var $ = require('../vendor/minified/minified').$; - +var _ = require('../vendor/minified/minified')._; /** * Attaches event methods to the context. * Call with ClayEvents.call(yourObject, $eventTarget) @@ -11,48 +11,51 @@ var $ = require('../vendor/minified/minified').$; */ function ClayEvents($eventTarget) { var self = this; - var _eventProxies = {}; + var _eventProxies = []; /** - * Attach an event listener to the item. This proxies minified.js' on. - * If you are using a native event like "change", consider using "|change" instead - * as this will allow the native events to still work - * @see {@link http://minifiedjs.com/api/on.html|.on()} + * prefixes events with "|" * @param {string} events - * @param {function} handler - * @returns {object} + * @returns {string} + * @private */ - self.on = function(events, handler) { - var _events = events.split(' ').map(function(event) { + var _transformEventNames = function(events) { + return events.split(' ').map(function(event) { return '|' + event.replace(/^\|/, ''); }).join(' '); - var self = this; - _eventProxies[handler] = function() { - handler.apply(self, arguments); - }; - $eventTarget.on(_events, _eventProxies[handler]); - return self; + }; + + var _registerEventProxy = function(handler, proxy) { + var eventProxy = _.find(_eventProxies, function(item) { + return item.handler === handler ? item : null; + }); + + if (!eventProxy) { + eventProxy = { handler: handler, proxy: proxy }; + _eventProxies.push(eventProxy); + } + return eventProxy.proxy; + }; + + var _getEventProxy = function(handler) { + return _.find(_eventProxies, function(item) { + return item.handler === handler ? item.proxy : null; + }); }; /** - * Attach an event listener to the item. This proxies minified.js' one. - * If you are using a native event like "change", consider using "|change" instead - * as this will allow the native events to still work - * @see {@link http://minifiedjs.com/api/one.html|.one()} - * @param {string} events + * Attach an event listener to the item. + * @param {string} events - a space separated list of events * @param {function} handler - * @returns {object} + * @returns {ClayEvents} */ - self.one = function(events, handler) { - var _events = events.split(' ').map(function(event) { - return '|' + event.replace(/^\|/, ''); - }).join(' '); + self.on = function(events, handler) { + var _events = _transformEventNames(events); var self = this; - _eventProxies[handler] = function(event) { + var _proxy = _registerEventProxy(handler, function() { handler.apply(self, arguments); - $.off(_eventProxies[handler]); - }; - $eventTarget.on(_events, _eventProxies[handler]); + }); + $eventTarget.on(_events, _proxy); return self; }; @@ -60,10 +63,13 @@ function ClayEvents($eventTarget) { * Remove the given event handler. * @see {@link http://minifiedjs.com/api/off.html|$.off()} * @param {function} handler - * @returns {object} + * @returns {ClayEvents} */ self.off = function(handler) { - $.off(_eventProxies[handler]); + var _proxy = _getEventProxy(handler); + if (_proxy) { + $.off(_proxy); + } return self; }; @@ -73,7 +79,7 @@ function ClayEvents($eventTarget) { * @param {object} [eventObj] - an object to pass to the event handler, * provided the handler does not have custom arguments. * @see {@link http://minifiedjs.com/api/trigger.html|.trigger()} - * @returns {object} + * @returns {ClayEvents} */ self.trigger = function(name, eventObj) { $eventTarget.trigger(name, eventObj); diff --git a/src/scripts/vendor/minified/minified.js b/src/scripts/vendor/minified/minified.js index 36a8715..05b4061 100644 --- a/src/scripts/vendor/minified/minified.js +++ b/src/scripts/vendor/minified/minified.js @@ -1,7 +1,8 @@ // minified.js config start -- use this comment to re-create a configuration in the Builder // - Only sections add, always, amdsupport, copyobj, dollardollar, -// - each, eachobj, error, extend, format, formathtml, get, ht, html, isobject, -// - off, on, ready, request, select, set, template, trigger, underscore, wait. +// - each, eachobj, error, extend, find, format, formathtml, get, ht, html, +// - isobject, off, on, ready, request, select, set, template, trigger, underscore, +// - wait. // WARNING! This file is autogenerated from minified-master.js and others. @@ -1549,6 +1550,69 @@ define('minified', function() { */ 'each': listBind(each), + /*$ + * @id find + * @group LIST + * @requires + * @configurable default + * @name .find() + * @altname _.find() + * @syntax list.find(findFunc) + * @syntax list.find(element) + * @syntax list.find(findFunc, startIndex) + * @syntax list.find(element, startIndex) + * @syntax _.find(list, findFunc) + * @syntax _.find(list, element) + * @syntax _.find(list, findFunc, startIndex) + * @syntax _.find(list, element, startIndex) + * @module WEB, UTIL + * Finds a specific value in the list. There are two ways of calling find(): + *
    + *
  1. With a value as argument. Then find() will search for the first occurrence of an identical value in the list, + * using the '===' operator for comparisons, and return the index. If it is not found, + * find() returns undefined.
  2. + *
  3. With a callback function. find() will then call the given function for each list element until the function + * returns a value that is not null or undefined. This value will be returned.
  4. + *
+ * + * find() can also be used as an alternative to ##each() if you need to abort the loop. + * + * @example Finds the first negative number in the list: + *
+     * var i = _(1, 2, -4, 5, 2, -1).find(function(value, index) { if (value < 0) return index; }); // returns 2
+     * 
+ + * @example Finds the index of the first 5 in the array: + *
+     * var i = _.find([3, 6, 7, 6, 5, 4, 5], 5); // returns 4 (index of first 5)
+     * 
+ * + * @example Determines the position of the element with the id '#wanted' among all li elements: + *
+     * var elementIndex = $('li').find($$('#wanted'));
+     * 
+ * + * @example Goes through the elements to find the first div that has the class 'myClass', and returns this element: + *
+     * var myClassElement = $('div').find(function(e) { if ($(e).is('.myClass')) return e; });
+     * 
+ * + * @param list A list to use as input. Can be an array, a ##list#Minified list## or any other array-like structure with + * length property. + * @param findFunc The callback function(item, index) that will be invoked for every list item until it returns a non-null value: + *
item
The current list element.
index
The second the zero-based index of the current element.
+ *
this
This list.
+ *
(callback return value)
If the callback returns something other than null or + * undefined, find() will return it directly. Otherwise it will continue.
+ * @param element the element to search for + * @param startIndex optional the 0-based index of the first element to search. + * @return if called with an element, either the element's index in the list or undefined if not found. If called with a callback function, + * it returns either the value returned by the callback or undefined. + * + * @see ##findLast() is the equivalent to find() for the list's end. + */ + 'find': listBind(find), + /*$ * @stop */ @@ -2559,6 +2623,9 @@ define('minified', function() { // @condblock each 'toObject': toObject, // @condend + // @condblock find + 'find': find, + // @condend /*$ * @id copyobj diff --git a/test/spec/lib/clay-config.js b/test/spec/lib/clay-config.js index 8ae5433..4eb5128 100644 --- a/test/spec/lib/clay-config.js +++ b/test/spec/lib/clay-config.js @@ -1,7 +1,7 @@ 'use strict'; var assert = require('chai').assert; -var sinon = require('sinon'); +var _ = require('../../../src/scripts/vendor/minified/minified')._; var textComponent = require('pebble-clay-components/dist/components/text'); var componentRegistry = require('../../../src/scripts/lib/component-registry'); var checkReadOnly = require('../../test-utils').checkReadOnly; @@ -18,7 +18,6 @@ describe('ClayConfig', function() { 'registerComponent', 'build', 'on', - 'one', 'off', 'trigger' ]; @@ -129,7 +128,37 @@ describe('ClayConfig', function() { clayConfig.build(); }); - // @todo test for validation + it('throws if manipulator is a string and does not match built-in manipulator', + function(done) { + var clayConfig = fixtures.clayConfig(['text'], true); + var _textComponent = _.copyObj(textComponent); + _textComponent.manipulator = 'not_real'; + + clayConfig.on(clayConfig.EVENTS.BEFORE_BUILD, function() { + assert.throws(function() { + clayConfig.registerComponent(_textComponent); + }, new RegExp('not_real')); + done(); + }); + + clayConfig.build(); + }); + + it('throws if manipulator does not have a `get` and `set` method', + function(done) { + var clayConfig = fixtures.clayConfig(['text'], true); + var _textComponent = _.copyObj(textComponent); + _textComponent.manipulator = {}; + + clayConfig.on(clayConfig.EVENTS.BEFORE_BUILD, function() { + assert.throws(function() { + clayConfig.registerComponent(_textComponent); + }, /(get.*set)|(set.*get)/); + done(); + }); + + clayConfig.build(); + }); }); describe('.build()', function() { diff --git a/test/spec/lib/clay-events.js b/test/spec/lib/clay-events.js new file mode 100644 index 0000000..78d3c2d --- /dev/null +++ b/test/spec/lib/clay-events.js @@ -0,0 +1,114 @@ +'use strict'; +var sinon = require('sinon'); +var assert = require('chai').assert; +var ClayEvents = require('../../../src/scripts/lib/clay-events'); +var $ = require('../../../src/scripts/vendor/minified/minified').$; +var HTML = require('../../../src/scripts/vendor/minified/minified').HTML; + +/** + * @extends ClayEvents + */ +var ctx; + +var eventCounter = 0; + +var createEventName = function() { + eventCounter++; + return 'test-event-' + eventCounter; +}; + +describe('ClayEvents', function() { + + beforeEach(function() { + ctx = {}; + ClayEvents.call(ctx, $(HTML('
'))); + }); + + it('registers the methods on the context', function() { + ['on', 'off', 'trigger'].forEach(function(method) { + assert.typeOf(ctx[method], 'function'); + }); + }); + + describe('.on()', function() { + it('registers one event', function() { + var eventName = createEventName(); + var eventHandlerSpy = sinon.spy(); + + ctx.on(eventName, eventHandlerSpy); + ctx.trigger(eventName); + ctx.trigger(eventName); + + assert(eventHandlerSpy.calledTwice, 'handler not called 2 times'); + assert(eventHandlerSpy.alwaysCalledOn(ctx), 'handler not called on ctx'); + }); + + it('registers multiple events', function() { + var eventName1 = createEventName(); + var eventName2 = createEventName(); + var eventHandlerSpy = sinon.spy(); + + ctx.on(eventName1 + ' ' + eventName2, eventHandlerSpy); + ctx.trigger(eventName1); + ctx.trigger(eventName1); + ctx.trigger(eventName2); + ctx.trigger(eventName2); + + assert.strictEqual(eventHandlerSpy.callCount, 4, 'handler not called 4 times'); + assert(eventHandlerSpy.alwaysCalledOn(ctx), 'handler not called on ctx'); + }); + }); + + describe('.off()', function() { + it('deregisters the handler for all events on the context', function() { + var eventName1 = createEventName(); + var eventName2 = createEventName(); + var eventHandlerSpy = sinon.spy(); + + var ctx1 = ctx; + var ctx2 = {}; + ClayEvents.call(ctx2, $(HTML('
'))); + + ctx.id = 1; + ctx1.on(eventName1, eventHandlerSpy); + ctx1.on(eventName2, eventHandlerSpy); + ctx2.on(eventName2, eventHandlerSpy); + + ctx1.trigger(eventName1); + ctx1.trigger(eventName2); + ctx2.trigger(eventName2); + + ctx1.off(eventHandlerSpy); + + ctx1.trigger(eventName1); + ctx1.trigger(eventName2); + ctx2.trigger(eventName2); + + assert.strictEqual(eventHandlerSpy.callCount, 4, 'handler not called 4 times'); + }); + + it('does nothing if the handler does not exist', function() { + // register a fake event so _getEventProxy() has something to look for + ctx.on(createEventName(), sinon.spy()); + + assert.doesNotThrow(function() { + ctx.off(sinon.spy()); + }); + }); + }); + + describe('.trigger()', function() { + it('triggers the handler for the event with custom data', function() { + var eventName = createEventName(); + var eventHandlerSpy = sinon.spy(); + var customData = {foo: 'bar'}; + + ctx.on(eventName, eventHandlerSpy); + ctx.trigger(eventName, customData); + + assert(eventHandlerSpy.calledOnce, 'handler not called 2 times'); + assert(eventHandlerSpy.alwaysCalledOn(ctx), 'handler not called on ctx'); + assert(eventHandlerSpy.calledWith(customData), 'handler not called on ctx'); + }); + }); +}); diff --git a/test/spec/lib/clay-item.js b/test/spec/lib/clay-item.js index b70a532..7b7ab48 100644 --- a/test/spec/lib/clay-item.js +++ b/test/spec/lib/clay-item.js @@ -17,7 +17,6 @@ describe('ClayItem', function() { '$element', '$manipulatorTarget', 'on', - 'one', 'off', 'trigger', 'initialize'