Permalink
Browse files

RedisRegistry working w/ mocha tests

  • Loading branch information...
1 parent e2c86fd commit 9f9e4a774108e44312ca911d7997d50a7b57fa66 @ryedin ryedin committed Oct 8, 2012
Showing with 595 additions and 122 deletions.
  1. +263 −0 lib/event-publisher-redis.js
  2. +194 −107 lib/registry-redis.js
  3. +2 −2 package.json
  4. +135 −12 test/redisRegistry.js
  5. +1 −1 test_legacy/unit/registry-test.js
View
263 lib/event-publisher-redis.js
@@ -0,0 +1,263 @@
+var base = require("./base-class"),
+ _ = require("underscore")._,
+ inherits = require("inherits"),
+ isArray = Array.isArray;
+
+module.exports = EventPublisher;
+
+/**
+ * Convert array-like object to an Array.
+ *
+ * node-bench: "16.5 times faster than Array.prototype.slice.call()"
+ *
+ * @param {Object} obj
+ * @return {Array}
+ * @api private
+ */
+function toArray(obj){
+ var len = obj.length,
+ arr = new Array(len);
+ for (var i = 0; i < len; ++i) {
+ arr[i] = obj[i];
+ }
+ return arr;
+}
+
+/**
+ * @class TODO: Complete description
+ * @param {Object} options
+ */
+function EventPublisher(options) {
+ base.apply(this, arguments); //get our minimal base class stuff (i.e. id, dispose)
+ EventPublisher.super.apply(this, arguments); //get the base node EventEmitter functionality
+
+ //allow wiring events via the constructor
+ if (options && (options.on || options.once)) {
+ if (options.on) {
+ for (var evt in options.on) {
+ this.on(evt, options.on[evt]);
+ }
+ }
+ if (options.once) {
+ for (var evt in options.once) {
+ this.once(evt, options.once[evt]);
+ }
+ }
+ }
+};
+/**
+ * @param {String} type the name of the event to fire/emit.
+ * @function
+ * @name EventPublisher.prototype.fire
+ */
+/**
+ * @param {String} type the name of the event to fire/emit.
+ * @function
+ */
+EventPublisher.prototype.emit = EventPublisher.prototype.fire = function (type) {
+ if (this._suppressAll
+ || (this._suppressOnceEvents && this._suppressOnceEvents[type])
+ || (this._suppressEvents && this._suppressEvents[type])) {
+
+ if (this._suppressOnceEvents) {
+ delete this._suppressOnceEvents[type];
+ }
+ //buffer events for re-emitting later if required
+ if (this._bufferedEvents && (this._bufferedEvents[type] || this._bufferAll)) {
+ this._buffer.push({type: type, args: toArray(arguments)});
+ }
+ return false;
+ }
+
+ //this is much better, and for now we'll keep it for convenience, but even this may eventually have to go for performance reasons,
+ //and in that case we'd be forcing listening level code to manually manage cases where it cares who the sender is
+ var args = toArray(arguments);
+ args.push(this);
+
+ return EventPublisher.super.prototype.emit.apply(this, args);
+};
+/**
+ * Registers a listener for an event.
+ * @param {String} type - the event name to listen for
+ * @param {Object} listener the listener to attach
+ * @param {Object} disposable
+ * @function
+ * @name EventPublisher.prototype.on
+ */
+/**
+ * Registers a listener for an event.
+ * @param {String} type - the event name to listen for
+ * @param {Object} listener the listener to attach
+ * @param {Object} disposable
+ * @function
+ */
+EventPublisher.prototype.addListener = EventPublisher.prototype.on = function (type, listener, disposable) {
+ //increase default "max listeners" setting for this higher level class
+ if (!this._events) this._events = {};
+ if (typeof this._events.maxListeners === "undefined") {
+ this._events.maxListeners = 1000;
+ }
+
+ /*
+ * auto cleanup functionality for disposable listeners
+ */
+ var me = this;
+ if (disposable && typeof disposable.once === "function") {
+ //NOTE: for the sake of this functionality, a "disposable" is an object that's expected to fire a "disposed" event when being disposed
+ //this allows you to pass in an object that may dispose and have the event handler cleanup be done
+ //automatically for you.
+ disposable.once("disposed", function() {
+ me.removeListener(type, listener);
+ });
+ }
+
+ return EventPublisher.super.prototype.on.apply(this, arguments);
+};
+/**
+ * convenience method for attaching one-time listeners
+ * @param {String} type - the event name to listen for
+ * @param {Object} listener the listener to attach
+ * @param {Object} disposable
+ */
+EventPublisher.prototype.once = function(type, listener, disposable) {
+ var me = this;
+ var cb = listener;
+ listener = function() {
+ me.removeListener(type, listener);
+ cb.apply(arguments.callee, arguments);
+ };
+ return this.on(type, listener, disposable);
+};
+
+/**
+ * allow events to be suppressed by event type, and optionally buffered for re-emitting after unsuppression
+ * pass null or nothing in for type param to indicate that all events should be suppressed
+ * note: if suppressing all events, the only way to unsuppress any events is to unsuppress them all.
+ * In other words, I'm doing no internal tracking/checking of "unsuppressed" event names against all possible events if _suppressAll is true
+ * (this is so the logic can remain fairly light; i.e. some edge cases are not currently supported)
+ * @param {String | String[]} type - the event name to suppress, or an array of event names to suppress
+ * @param {Boolean} buffer - flag to indicate whether the suppressed events should be buffered for re-emission after unsuppress
+ * @param {Boolean} once - flag to indicate whether the event(s) should only be suppressed one time
+ */
+EventPublisher.prototype.suppress = function(type, buffer, once) {
+ this._suppressEvents = this._suppressEvents || {};
+ this._suppressOnceEvents = this._suppressOnceEvents || {};
+ var suppressionTarget = once ? this._suppressOnceEvents : this._suppressEvents;
+ //instance level queue for all events so re-firing mixed suppressed events can be in the original order
+ buffer && (this._buffer = this._buffer || []);
+ //state object to track events that should be buffered
+ buffer && (this._bufferedEvents = this._bufferedEvents || {});
+ if (typeof type === "undefined" || type === null) {
+ this._suppressAll = true;
+ buffer && (this._bufferAll = true);
+ } else {
+ if (isArray(type)) {
+ for (var i = 0, l = type.length; i < l; i++) {
+ suppressionTarget[type[i]] = true;
+ buffer && (this._bufferedEvents[type[i]] = true);
+ }
+ } else {
+ suppressionTarget[type] = true;
+ buffer && (this._bufferedEvents[type] = true);
+ }
+ }
+};
+
+/**
+ * convenience alias for suppressing events one time
+ * @param {String | String[]} type - the event name to suppress, or an array of event names to suppress
+ * @param {Boolean} buffer - flag to indicate whether the suppressed events should be buffered for re-emission after unsuppress
+ */
+EventPublisher.prototype.suppressOnce = function(type, buffer) {
+ this.suppress(type, buffer, true);
+};
+
+/**
+ * un-suppress events by event type, optionally re-emitting any buffered events in the process
+ * @param {String | String[]} type - the event name to un-suppress, or an array of event names to un-suppress
+ * @param {Boolean} bypassRefiring - flag to indicate whether buffered events should be re-emitted or not (true means events are NOT re-emitted. defaults to false)
+ */
+EventPublisher.prototype.unsuppress = function(type, bypassRefiring) {
+ //sanity assignments of the state objects to defend against case where .unsuppress is called before .suppress is ever called
+ this._suppressEvents = this._suppressEvents || {};
+ this._suppressOnceEvents = this._suppressOnceEvents || {};
+ bypassRefiring = !!bypassRefiring; //normalize to proper bool
+ var unsuppressAll = (typeof type === "undefined" || type === null);
+ if (unsuppressAll) {
+ //reset/remove all state flags
+ this._suppressAll = false;
+ this._bufferAll = false;
+ delete this._suppressEvents;
+ delete this._suppressOnceEvents;
+ delete this._bufferedEvents;
+ } else if (!this._suppressAll) {
+ //normalize type to an array
+ type = isArray(type) ? type : [type];
+ var currType;
+ for (var i = 0, l = type.length; i < l; i++) {
+ currType = type[i];
+ delete this._suppressEvents[currType];
+ delete this._suppressOnceEvents[currType];
+ if (bypassRefiring && this._buffer) {
+ this._buffer = _.filter(function(bufferEvent) {
+ return bufferEvent.type !== currType;
+ });
+ }
+ }
+ } else {
+ //if we're currently suppressing all events, and .unsuppress is called with a granular event name or event list,
+ //since we're not supporting this case (see comments for .suppress method), throw an error to alert
+ //the developer that this is not a supported case currently and they should change their code.
+ throw new Error("Cannot unsuppress a subset of events when the EventPublisher is currenly suppressing ALL events. Please change this call to unsuppress all events by passing either nothing or null in as the first argument.");
+ }
+
+ //if we got this far, now re-emit all appropriate buffered events
+ if (!bypassRefiring && this._buffer) {
+ //do the loop thing
+ var currEvent,
+ length = this._buffer.length, //cache original length so we can stop the loop at the right spot if events get re-queued (which is possible for some supported cases)
+ index = 0,
+ offset = 0,
+ mismatches = {}, //a cache to index mismatching types to minimize .indexOf lookups in the loop
+ matches = {}; //a cache to index the types after first matching lookup to minimize .indexOf calls in the loop
+ while (this._buffer && index < length) {
+ if (this._buffer.length > 0) {
+ currEvent = this._buffer[offset];
+ //re-emit the event if type matches one of the passed in types or if we're unsuppressing all
+ //note: even though suppress could be re-called during this process,
+ //we'll go through the whole loop (up to the original length of the buffer)
+ //this supports the case where a subset of events are re-suppressed and some of them are not
+ if (!mismatches[currEvent.type]
+ && (unsuppressAll
+ || matches[currEvent.type]
+ || type.indexOf(currEvent.type) > -1)) {
+ //cache this type to avoid an indexOf lookup next time the same type is checked
+ matches[currEvent.type] = true;
+ //take it out of the buffer
+ this._buffer.splice(offset, 1);
+ //finally, re-emit unless told not to at the call level
+ !bypassRefiring && this.fire.apply(this, currEvent.args);
+ } else {
+ //mark this event type as a mismatch to avoid unneccesary lookup/checks next time one is found
+ mismatches[currEvent.type] = true;
+ offset++;
+ }
+ if (this._buffer.length == 0) delete this._buffer;
+ index++;
+ } else {
+ delete this._buffer;
+ }
+ }
+ }
+};
+
+/**
+ * Disposes of all listeners and fires "disposed" event before calling super.dispose.
+ */
+EventPublisher.prototype.dispose = function() {
+ this.fire("disposed");
+ this.removeAllListeners();
+ base.prototype.dispose.apply(this, arguments);
+};
+
+inherits(EventPublisher, process.EventEmitter);
View
301 lib/registry-redis.js
@@ -1,184 +1,271 @@
var inherits = require("inherits"),
_ = require("underscore")._,
redis = require("redis"),
- Registry = require('./registry');
+ EventPublisher = require('./event-publisher');
module.exports = RedisRegistry;
-/**
- * @class Registry is an iterable collection of objects keyed by an ID. It internally stores
- * both an Array of items ordered by order added, as well as an associative array (Object) where
- * the items are indexed by the idKey of the Registry for fast lookups. Events are fired as items
- * are added or removed from the registry. The idKey is configurable, and defaults to 'id'. All items
- * added to a Registry must have a property whose key matches the idKey of the registry. All items
- * in the Registry must also have a unique idKey property value.
- * @param {Object} options
- * @param {Object} defaults
- * @extends EventPublisher
- */
-function RedisRegistry(options, defaults) {
- RedisRegistry.super.call(this, options, defaults);
-
- this.items = []; //iterable collection
- this.itemCache = {}; //indexed collection
+function RedisRegistry(options) {
+ RedisRegistry.super.call(this, _.extend({
+ idKey: "id",
+ uniqueErrorMessage: "All items in this registry instance must have unique IDs."
+ }, options || {}));
+
+ //setup a redis client
+ this.client = redis.createClient(
+ this.options.redisPort, //default = 6379
+ this.options.redisHost, //default = 127.0.0.1
+ this.options.redisOptions //default = see https://github.com/mranney/node_redis#rediscreateclientport-host-options
+ );
+
+ //create a redis hash key for this registry
+ this.hashId = 'redis-registry::' + this.id;
}
RedisRegistry.prototype = {
- /**
- * Add an item to the registry
- * @param {Object} item Item to be added
- * @param {Integer} index Optional, where to insert the item into the list of registry items
- * @return {Boolean}
- */
- add: function(item, index) {
- var idKey = this.options.idKey,
- options = this.options;
+
+ add: function(item, cb) {
+ var me = this,
+ idKey = this.options.idKey,
+ options = this.options,
+ itemJSON;
+
if (item[idKey]) {
- if (typeof this.itemCache[item[idKey]] !== "undefined") {
- throw new Error(options.uniqueErrorMessage + "... id: " + item[idKey]);
- }
- //add to iterable collection, with support for choosing where in the collection to add it
- if (index !== undefined && !isNaN(index)) {
- if (this.items.length == 0) {
- this.items.push(item);
- }
- else {
- var tmpItems = [];
- index = index < 0 ? 0 : (index > this.items.length - 1 ? this.items.length - 1 : index);
- this.each(function(h, _index){
- if (_index == index) {
- tmpItems.push(item);
- }
- tmpItems.push(h);
- });
- this.items = tmpItems;
- }
+ //items MUST be JSON serializable for this registry
+ try {
+ itemJSON = JSON.stringify(item);
+ } catch (ex) {
+ itemJSON = null;
+ cb('Error serializing item to add to registry.', ex);
}
- else {
- this.items.push(item);
+
+ if (itemJSON) {
+ //enforce unique keys (only set value if hash key does not yet exist)
+ me.client.hsetnx(me.hashId, item[idKey], itemJSON, function(err, result) {
+ if (err) cb('Error adding item to registry.', err); else {
+
+ //result == 1 if item was successfully set, 0 otherwise
+ if (!result) {
+ cb(options.uniqueErrorMessage + "... id: " + item[idKey]);
+ } else {
+
+ //TODO: the events need to be backed by redis also (via redis pub/sub)
+ me.fire("itemAdded", item);
+ cb();
+ }
+ }
+ });
}
- //add to indexed collection
- this.itemCache[item[idKey]] = item;
-
- this.fire("itemAdded", item);
} else {
- throw new Error("Items in this registry must have '" + idKey + "' properties in order to be added.");
+ cb("Items in this registry must have '" + idKey + "' properties in order to be added.");
}
- return true;
},
/**
* Copy items from one collection based object (basically any object that supports .each enumeration)
* into this registry instance
* @param {Array} collection
*/
- addRange: function(collection) {
- var me = this;
+ addRange: function(collection, cb) {
+ var me = this,
+ errors = [],
+ sem = 0;
+
_.each(collection, function(item) {
- me.add(item);
+ sem++;
+ me.add(item, function(err) {
+ if (err) errors.push(err);
+
+ sem--;
+ if (sem == 0) {
+ if (errors.length) {
+ cb(errors);
+ } else {
+ cb();
+ }
+ }
+ });
});
},
/**
* Finds an item in the registry
* @param {Function} iterator Function which returns true when the desired item is found
- * @returns {Object} The item
*/
- find: function(iterator) {
- return _.find(this.items, iterator);
+ find: function(iterator, cb) {
+ //TODO: currently this will be pretty inefficient due to buffering all and deserializing. May need to investigate better way.
+ this.client.hvals(this.hashId, function(err, itemsJSON) {
+ if (err) cb(err); else {
+ try {
+ var items = _.map(itemsJSON, function(itemJSON) {
+ return JSON.parse(itemJSON);
+ });
+ cb(null, _.find(items, iterator));
+ } catch (ex) {
+ cb('Error parsing items during find.', ex);
+ }
+ }
+ });
+ },
+
+ length: function(cb) {
+ this.client.hlen(this.hashId, cb);
},
/**
+ * Returns all items in the registry
+ * @param {Function} iterator Function which returns true when the desired item is found
+ */
+ getAll: function(cb) {
+ //TODO: currently this will be pretty inefficient due to buffering all and deserializing. May need to investigate better way.
+ this.client.hvals(this.hashId, function(err, itemsJSON) {
+ if (err) cb(err); else {
+ try {
+ var items = _.map(itemsJSON, function(itemJSON) {
+ return JSON.parse(itemJSON);
+ });
+ cb(null, items);
+ } catch (ex) {
+ cb('Error parsing items during getAll.', ex);
+ }
+ }
+ });
+ },
+
+ /**
* Finds all matching items in the registry
* @param {Function} iterator Function which returns true when the desired item is found
- * @returns {Array} The matching items, or null if none found
*/
- findAll: function(iterator) {
- return _.filter(this.items, iterator);
+ findAll: function(iterator, cb) {
+ this.getAll(function(err, items) {
+ if (err) cb(err); else {
+ cb(null, _.filter(items, iterator));
+ }
+ });
},
/**
* Finds an item in the registry by ID
*
- * NOTE: in its current form this is simply an alias for:
- * var item = someRegistry.itemCache["foo"];
- *
- * Leaving it here to support legacy code, and also so we can potentially add more complex
- * indexing logic support at a later time.
- *
* @param {String} id ID of the registry item
- * @returns {Object} The item found at the given index (undefined if none exist)
*/
- findById: function(id) {
- return this.itemCache[id];
+ findById: function(id, cb) {
+ this.client.hget(this.hashId, id, function(err, itemJSON) {
+ if (err) cb(err); else {
+ if (!itemJSON) cb(null, null); else {
+ try {
+ var item = JSON.parse(itemJSON);
+ cb(null, item);
+ } catch (ex) {
+ cb('Error parsing item during findById.', ex);
+ }
+ }
+ }
+ });
},
/**
* Removes an item from the registry by ID
* @param {String} id ID of the registry item
- * @returns {Boolean}
*/
- removeById: function(id) {
- var item = this.findById(id);
- return this.remove(item);
+ removeById: function(id, cb) {
+ var me = this;
+ //get the item first for event firing
+ this.findById(id, function(err, item) {
+ if (err) cb(err); else {
+ if (!item) cb(null, false); else {
+ me.client.hdel(me.hashId, id, function(err, result) {
+ if (err) cb(err); else if (result) {
+
+ //TODO: use redis pub/sub to back the events... (need to distribute to any other connected processes)
+ me.fire('itemRemoved', item);
+ //check for cleared status
+ me.client.hlen(me.hashId, function(err, len) {
+ if (!err && !len) {
+ me.fire('cleared');
+ }
+
+ cb(err, true);
+ });
+ } else {
+
+ cb(null, false);
+ }
+ });
+ }
+ }
+ })
},
/**
* Removes an item from the registry
* @param {Object} item The registry item
- * @returns {Boolean}
*/
- remove: function(item) {
- if (!item) {
- return false;
- }
- var foundItem = false;
- this.items = _.reject(this.items, function(it) {
- if (it === item) {
- foundItem = true;
- return true;
- }
- return false;
- });
- delete this.itemCache[item[this.options.idKey]];
- if (foundItem) {
- this.fire("itemRemoved", item);
- if (this.items.length == 0) {
- this.fire("cleared");
- }
- return true;
- }
- return false;
+ remove: function(item, cb) {
+ this.removeById(item[this.options.idKey], cb);
},
/**
* Removes all items from the registry, with events
*/
- removeAll: function() {
- var me = this;
- this.each(function(item) {
- me.remove(item);
+ removeAll: function(cb) {
+ var me = this,
+ sem = 0;
+
+ //don't use each here because we only need the keys (each adds parsing overhead)
+ this.client.hkeys(this.hashId, function(err, keys) {
+ if (err) cb(err); else {
+ if (!keys || !keys.length) cb(); else {
+ var errors = [];
+ _.each(keys, function(key) {
+ sem++;
+ me.removeById(key, function(err) {
+ if (err) errors.push(err);
+
+ sem--;
+ if (sem == 0) {
+ if (!errors.length) errors = null;
+ cb(errors);
+ }
+ });
+ });
+ }
+ }
});
},
/**
* Performs an action on each item in the registry
* @param {Function} iterator Function containing code to execute on each item
*/
- each: function(iterator) {
- _.each(this.items, iterator);
+ each: function(iterator, cb) {
+ var me = this;
+
+ this.getAll(function(err, items) {
+ if (err) cb(err); else {
+ _.each(items, iterator);
+ cb();
+ }
+ });
},
/**
* Disposes of this registry.
* @param {Object} $super
*/
- dispose: function() {
- this.removeAll();
- RedisRegistry.super.prototype.dispose.apply(this, arguments);
+ dispose: function(cb) {
+ var me = this,
+ args = _.toArray(arguments);
+
+ this.removeAll(function(err) {
+ RedisRegistry.super.prototype.dispose.apply(me, args);
+
+ //TODO: remove this once RedisEventPublisher is done (cb will be called in that dispose method then)
+ process.nextTick(cb);
+ });
}
};
-inherits(RedisRegistry, Registry);
+inherits(RedisRegistry, EventPublisher);
View
4 package.json
@@ -27,8 +27,8 @@
"connect-redis": "1.4.1"
},
"devDependencies": {
- "mocha": "1.2.x",
- "should": "0.6.x"
+ "mocha": "1.6.0",
+ "should": "1.2.0"
},
"repository": {
"type": "git",
View
147 test/redisRegistry.js
@@ -1,30 +1,153 @@
-var RedisRegistry = require('../lib/registry-redis');
+var RedisRegistry = require('../lib/registry-redis'),
+ should = require('should');
+
+var registry;
describe('RedisRegistry Tests', function() {
//setup --------------------------------------
beforeEach(function(done) {
-
- done();
+ //create/dispose/create assures items are cleared out of redis from a previous failed test
+ registry = new RedisRegistry({
+ id: 'test'
+ });
+ registry.dispose(function() {
+ registry = new RedisRegistry({
+ id: 'test'
+ });
+ done();
+ });
});
//teardown --------------------------------------
afterEach(function(done) {
+ registry.dispose(done);
+ });
+
+
+ it('should add an item', function(done) {
+
+ var item1 = {id: "item1"};
+
+ registry.add(item1, function(err) {
+
+ should.not.exist(err);
+ registry.length(function(err, len) {
+
+ should.not.exist(err);
+ len.should.equal(1);
+
+ registry.findById(item1.id, function(err, item) {
+
+ should.not.exist(err);
+ item.should.eql(item1);
+ done();
+ });
+ });
+ });
- done();
});
+ it('should not allow duplicate items', function(done) {
- it('should have 100 platform dubloons', function(done) {
+ var item1 = {id: "item1"};
+ var item2 = {id: "item1"};
- rise.api.walletTransaction.getBalance({
- currency: { type: 'platform', typeId: testData.platform._id, name: 'dubloons' },
- account: { type: 'user', typeId: testData.riseUser._id }
- }, function(err, result) {
+ registry.add(item1, function(err) {
- should.equal(200, result);
- done();
- })
+ should.not.exist(err);
+ registry.add(item2, function(err) {
+
+ should.exist(err);
+ err.should.equal("All items in this registry instance must have unique IDs.... id: item1");
+ done();
+ });
+ });
+
+ });
+
+ it('should add items from a range', function(done) {
+
+ var items = [
+ {id: "item1"},
+ {id: "item2"}
+ ];
+ var more_items = [
+ {id: "item3"},
+ {id: "item4"}
+ ];
+
+ registry.addRange(items, function(err) {
+
+ should.not.exist(err);
+ registry.length(function(err, len) {
+
+ should.not.exist(err);
+ len.should.equal(2);
+
+ registry.addRange(more_items, function(err) {
+
+ should.not.exist(err);
+ registry.length(function(err, len) {
+
+ should.not.exist(err);
+ len.should.equal(4);
+ done();
+ });
+ });
+ });
+ });
+
+ });
+
+ it('should remove an item', function(done) {
+
+ var item1 = {id: "item1"};
+ registry.add(item1, function(err) {
+
+ should.not.exist(err);
+ registry.length(function(err, len) {
+
+ should.not.exist(err);
+ len.should.equal(1);
+
+ registry.remove(item1, function(err, removed) {
+
+ should.not.exist(err);
+ removed.should.equal(true);
+
+ registry.length(function(err, len) {
+
+ should.not.exist(err);
+ len.should.equal(0);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it('should add and find by custom idKey', function(done) {
+
+ var item1 = {customId: 'item1'};
+ registry.options.idKey = 'customId';
+
+ registry.add(item1, function(err) {
+
+ should.not.exist(err);
+ registry.length(function(err, len) {
+
+ should.not.exist(err);
+ len.should.equal(1);
+
+ registry.findById(item1.customId, function(err, item) {
+
+ should.not.exist(err);
+ item.should.eql(item1);
+ done();
+ });
+ });
+ });
});
View
2 test_legacy/unit/registry-test.js
@@ -32,7 +32,7 @@
try {
this.registry.add(item2);
} catch (ex) {
- var err = "Error: All items in this registry instance must have unique IDs.\n\nid: item1";
+ var err = "Error: All items in this registry instance must have unique IDs.... id: item1";
Y.Assert.areEqual(ex.message, err, "no duplicate ids allowed");
}
},

0 comments on commit 9f9e4a7

Please sign in to comment.