Browse Source

Merge pull request #78 from ckeditor/t/77

Reimplemented the Collection class.
Piotr Jasiun 10 years ago
parent
commit
0b85778a91

+ 169 - 54
packages/ckeditor5-utils/src/collection.js

@@ -6,9 +6,14 @@
 'use strict';
 
 /**
- * Collections are ordered sets of models.
+ * Collections are ordered sets of objects. Items in the collection can be retrieved by their indexes
+ * in the collection (like in an array) or by their ids.
  *
- * See also {@link core/NamedCollection}.
+ * If an object without an `id` property is being added to the collection, the `id` property will be generated
+ * automatically. Note that the automatically generated id is unique only within this single collection instance.
+ *
+ * By default an item in the collection is identified by its `id` property. The name of the identifier can be
+ * configured through the constructor of the collection.
  *
  * @class Collection
  * @mixins EventEmitter
@@ -20,15 +25,42 @@ CKEDITOR.define( [ 'emittermixin', 'ckeditorerror', 'utils' ], function( Emitter
 		 * Creates a new Collection instance.
 		 *
 		 * @constructor
+		 * @param {Iterale} [items] Items to be added to the collection.
+		 * @param {Object} options The options object.
+		 * @param {String} [options.idProperty='id'] The name of the property which is considered to identify an item.
 		 */
-		constructor() {
+		constructor( options ) {
+			/**
+			 * The internal list of items in the collection.
+			 *
+			 * @private
+			 * @property {Object[]}
+			 */
+			this._items = [];
+
+			/**
+			 * The internal map of items in the collection.
+			 *
+			 * @private
+			 * @property {Map}
+			 */
+			this._itemMap = new Map();
+
+			/**
+			 * Next id which will be assigned to unidentified item while adding it to the collection.
+			 *
+			 * @private
+			 * @property
+			 */
+			this._nextId = 0;
+
 			/**
-			 * The internal list of models in the collection.
+			 * The name of the property which is considered to identify an item.
 			 *
-			 * @property _models
 			 * @private
+			 * @property {String}
 			 */
-			this._models = [];
+			this._idProperty = options && options.idProperty || 'id';
 		}
 
 		/**
@@ -37,106 +69,189 @@ CKEDITOR.define( [ 'emittermixin', 'ckeditorerror', 'utils' ], function( Emitter
 		 * @property length
 		 */
 		get length() {
-			return this._models.length;
+			return this._items.length;
 		}
 
 		/**
 		 * Adds an item into the collection.
 		 *
-		 * Note that this is an array-like collection, so the same item can be present more than once. This behavior is
-		 * for performance purposes only and is not guaranteed to be kept in the same way in the future.
+		 * If the item does not have an id, then it will be automatically generated and set on the item.
 		 *
-		 * @param {Model} model The item to be added.
+		 * @chainable
+		 * @param {Object} item
 		 */
-		add( model ) {
-			this._models.push( model );
+		add( item ) {
+			var itemId;
+			var idProperty = this._idProperty;
+
+			if ( ( idProperty in item ) ) {
+				itemId = item[ idProperty ];
+
+				if ( typeof itemId != 'string' ) {
+					/**
+					 * This item's id should be a string.
+					 *
+					 * @error collection-add-invalid-id
+					 */
+					throw new CKEditorError( 'collection-add-invalid-id' );
+				}
+
+				if ( this.get( itemId ) ) {
+					/**
+					 * This item already exists in the collection.
+					 *
+					 * @error collection-add-item-already-exists
+					 */
+					throw new CKEditorError( 'collection-add-item-already-exists' );
+				}
+			} else {
+				itemId = this._getNextId();
+				item[ idProperty ] = itemId;
+			}
 
-			this.fire( 'add', model );
+			this._items.push( item );
+			this._itemMap.set( itemId, item );
+
+			this.fire( 'add', item );
+
+			return this;
 		}
 
 		/**
-		 * Gets one item from the collection.
+		 * Gets item by its id or index.
 		 *
-		 * @param {Number} index The index to take the item from.
-		 * @returns {Model} The requested item or `null` if such item does not exist.
+		 * @param {String|Number} idOrIndex The item id or index in the collection.
+		 * @returns {Object} The requested item or `null` if such item does not exist.
 		 */
-		get( index ) {
-			return this._models[ index ] || null;
+		get( idOrIndex ) {
+			var item;
+
+			if ( typeof idOrIndex == 'string' ) {
+				item = this._itemMap.get( idOrIndex );
+			} else if ( typeof idOrIndex == 'number' ) {
+				item = this._items[ idOrIndex ];
+			} else {
+				/**
+				 * Index or id must be given.
+				 *
+				 * @error collection-get-invalid-arg
+				 */
+				throw new CKEditorError( 'collection-get-invalid-arg: Index or id must be given.' );
+			}
+
+			return item || null;
 		}
 
 		/**
 		 * Removes an item from the collection.
 		 *
-		 * @param {Model|Number} modelOrIndex Either the item itself or its index inside the collection.
-		 * @returns {Model} The removed item.
+		 * @param {Object|Number|String} subject The item to remove, its id or index in the collection.
+		 * @returns {Object} The removed item.
 		 */
-		remove( modelOrIndex ) {
-			// If a model has been passed, convert it to its index.
-			if ( typeof modelOrIndex != 'number' ) {
-				modelOrIndex = this._models.indexOf( modelOrIndex );
+		remove( subject ) {
+			var index, id, item;
+			var itemDoesNotExist = false;
+			var idProperty = this._idProperty;
 
-				if ( modelOrIndex == -1 ) {
-					/**
-					 * Model not found.
-					 *
-					 * @error collection-model-404
-					 */
-					throw new CKEditorError( 'collection-model-404: Model not found.' );
+			if ( typeof subject == 'string' ) {
+				id = subject;
+				item = this._itemMap.get( id );
+				itemDoesNotExist = !item;
+
+				if ( item ) {
+					index = this._items.indexOf( item );
 				}
-			}
+			} else if ( typeof subject == 'number' ) {
+				index = subject;
+				item = this._items[ index ];
+				itemDoesNotExist = !item;
 
-			var removedModel = this._models.splice( modelOrIndex, 1 )[ 0 ];
+				if ( item ) {
+					id = item[ idProperty ];
+				}
+			} else {
+				item = subject;
+				id = item[ idProperty ];
+				index = this._items.indexOf( item );
+				itemDoesNotExist = ( index == -1 || !this._itemMap.get( id ) );
+			}
 
-			if ( !removedModel ) {
+			if ( itemDoesNotExist ) {
 				/**
-				 * Index not found.
+				 * Item not found.
 				 *
-				 * @error collection-index-404
+				 * @error collection-remove-404
 				 */
-				throw new CKEditorError( 'collection-index-404: Index not found.' );
+				throw new CKEditorError( 'collection-remove-404: Item not found.' );
 			}
 
-			this.fire( 'remove', removedModel );
+			this._items.splice( index, 1 );
+			this._itemMap.delete( id );
+
+			this.fire( 'remove', item );
 
-			return removedModel;
+			return item;
 		}
 
 		/**
-		 * Executes the callback for each model in the collection.
+		 * Executes the callback for each item in the collection and composes an array or values returned by this callback.
 		 *
 		 * @param {Function} callback
-		 * @param {Model} callback.item
-		 * @param {String} callback.index
+		 * @param {Item} callback.item
+		 * @param {Number} callback.index
 		 * @params {Object} ctx Context in which the `callback` will be called.
+		 * @returns {Array} The result of mapping.
 		 */
-		forEach( callback, ctx ) {
-			this._models.forEach( callback, ctx );
+		map( callback, ctx ) {
+			return this._items.map( callback, ctx );
 		}
 
 		/**
 		 * Finds the first item in the collection for which the `callback` returns a true value.
 		 *
 		 * @param {Function} callback
-		 * @param {Model} callback.item
-		 * @param {String} callback.name
-		 * @returns {Model} The item for which `callback` returned a true value.
+		 * @param {Object} callback.item
+		 * @param {Number} callback.index
+		 * @returns {Object} The item for which `callback` returned a true value.
 		 * @params {Object} ctx Context in which the `callback` will be called.
 		 */
 		find( callback, ctx ) {
-			return this._models.find( callback, ctx );
+			return this._items.find( callback, ctx );
 		}
 
 		/**
 		 * Returns an array with items for which the `callback` returned a true value.
 		 *
 		 * @param {Function} callback
-		 * @param {Model} callback.item
-		 * @param {String} callback.name
+		 * @param {Object} callback.item
+		 * @param {Number} callback.index
 		 * @params {Object} ctx Context in which the `callback` will be called.
-		 * @returns {Model[]} The array with matching items.
+		 * @returns {Object[]} The array with matching items.
 		 */
 		filter( callback, ctx ) {
-			return this._models.filter( callback, ctx );
+			return this._items.filter( callback, ctx );
+		}
+
+		/**
+		 * Collection iterator.
+		 */
+		[ Symbol.iterator ]() {
+			return this._items[ Symbol.iterator ]();
+		}
+
+		/**
+		 * Generates next (not yet used) id for unidentified item being add to the collection.
+		 *
+		 * @returns {String} The next id.
+		 */
+		_getNextId() {
+			var id;
+
+			do {
+				id = String( this._nextId++ );
+			} while ( this._itemMap.has( id ) );
+
+			return id;
 		}
 	}
 
@@ -149,12 +264,12 @@ CKEDITOR.define( [ 'emittermixin', 'ckeditorerror', 'utils' ], function( Emitter
  * Fired when an item is added to the collection.
  *
  * @event add
- * @param {Model} model The added item.
+ * @param {Object} item The added item.
  */
 
 /**
  * Fired when an item is removed from the collection.
  *
  * @event remove
- * @param {Model} model The removed item.
+ * @param {Object} item The removed item.
  */

+ 0 - 195
packages/ckeditor5-utils/src/namedcollection.js

@@ -1,195 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-/**
- * Named collections are key => model maps.
- *
- * See also {@link core/Collection}.
- *
- * @class NamedCollection
- * @mixins EventEmitter
- */
-
-CKEDITOR.define( [ 'emittermixin', 'ckeditorerror', 'utils' ], function( EmitterMixin, CKEditorError, utils ) {
-	class NamedCollection {
-		/**
-		 * Creates a new NamedCollection instance.
-		 *
-		 * @constructor
-		 */
-		constructor() {
-			/**
-			 * The internal map of models in the collection.
-			 *
-			 * @property _models
-			 * @private
-			 */
-			this._models = new Map();
-		}
-
-		/**
-		 * The number of items available in the collection.
-		 *
-		 * @property length
-		 */
-		get length() {
-			return this._models.size;
-		}
-
-		/**
-		 * Adds an item into the collection.
-		 *
-		 * Throws exception if an item with this name already exists or if the item does not have a name.
-		 *
-		 * @param {Model} model The item to be added.
-		 */
-		add( model ) {
-			var name = model.name;
-
-			if ( !name || this._models.has( name ) ) {
-				/**
-				 * Model isn't named or such model already exists in this collection
-				 *
-				 * Thrown when:
-				 *
-				 * * Model without a name was given.
-				 * * Model with this name already exists in the collection.
-				 *
-				 * @error namedcollection-add
-				 * @param {String} name Name of the model.
-				 */
-				throw new CKEditorError(
-					'namedcollection-add: Model isn\'t named or such model already exists in this collection',
-					{ name: name }
-				);
-			}
-
-			this._models.set( name, model );
-
-			this.fire( 'add', model );
-		}
-
-		/**
-		 * Gets one item from the collection.
-		 *
-		 * @param {String} name The name of the item to take.
-		 * @returns {Model} The requested item or `null` if such item does not exist.
-		 */
-		get( name ) {
-			return this._models.get( name ) || null;
-		}
-
-		/**
-		 * Removes an item from the collection.
-		 *
-		 * @param {Model|String} modelOrName Either the item itself (it must have a `name` property)
-		 * or its name inside the collection.
-		 * @returns {Model} The removed item.
-		 */
-		remove( modelOrName ) {
-			var nameGiven = typeof modelOrName == 'string';
-			var name = nameGiven ? modelOrName : modelOrName.name;
-			var model = this._models.get( name );
-
-			if ( nameGiven ? !model : ( model !== modelOrName ) ) {
-				/**
-				 * Model not found or other model exists under its name.
-				 *
-				 * Thrown when:
-				 *
-				 * * a model without a name was given,
-				 * * no model found when a name was given,
-				 * * a model was given and it does not exist in the collection or some other model was found under its name.
-				 *
-				 * @error namedcollection-remove
-				 * @param {String} name Name of the model to remove.
-				 * @param {Model} model The model which was found under the given name.
-				 */
-				throw new CKEditorError(
-					'namedcollection-remove: Model not found or other model exists under its name.',
-					{ name: name, model: model }
-				);
-			}
-
-			this._models.delete( name );
-
-			this.fire( 'remove', model );
-
-			return model;
-		}
-
-		/**
-		 * Executes the callback for each model in the collection.
-		 *
-		 * @param {Function} callback
-		 * @param {Model} callback.item
-		 * @param {String} callback.index
-		 * @params {Object} ctx Context in which the `callback` will be called.
-		 */
-		forEach( callback, ctx ) {
-			this._models.forEach( callback, ctx );
-		}
-
-		/**
-		 * Finds the first item in the collection for which the `callback` returns a true value.
-		 *
-		 * @param {Function} callback
-		 * @param {Model} callback.item
-		 * @param {String} callback.name
-		 * @params {Object} ctx Context in which the `callback` will be called.
-		 * @returns {Model} The first item for which `callback` returned a true value.
-		 */
-		find( callback, ctx ) {
-			// TODO: Use ES6 destructuring.
-			for ( let pair of this._models ) {
-				if ( callback.call( ctx, pair[ 1 ], pair[ 0 ] ) ) {
-					return pair[ 1 ];
-				}
-			}
-		}
-
-		/**
-		 * Returns an object (`name => item`) with items for which the `callback` returned a true value.
-		 *
-		 * @param {Function} callback
-		 * @param {Model} callback.item
-		 * @param {String} callback.name
-		 * @params {Object} ctx Context in which the `callback` will be called.
-		 * @returns {Object} The object with matching items.
-		 */
-		filter( callback, ctx ) {
-			var ret = {};
-
-			// TODO: Use ES6 destructuring.
-			for ( let pair of this._models ) {
-				if ( callback.call( ctx, pair[ 1 ], pair[ 0 ] ) ) {
-					ret[ pair[ 0 ] ] = pair[ 1 ];
-				}
-			}
-
-			return ret;
-		}
-	}
-
-	utils.extend( NamedCollection.prototype, EmitterMixin );
-
-	return NamedCollection;
-} );
-
-/**
- * Fired when an item is added to the collection.
- *
- * @event add
- * @param {Model} model The added item.
- */
-
-/**
- * Fired when an item is removed from the collection.
- *
- * @event remove
- * @param {Model} model The removed item.
- */

+ 390 - 0
packages/ckeditor5-utils/tests/collection/collection.js

@@ -0,0 +1,390 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+var modules = bender.amd.require( 'collection', 'ckeditorerror' );
+
+bender.tools.createSinonSandbox();
+
+function getItem( id, idProperty ) {
+	return {
+		[ idProperty || 'id' ]: id
+	};
+}
+
+describe( 'Collection', () => {
+	var Collection, CKEditorError;
+
+	before( () => {
+		Collection = modules.collection;
+		CKEditorError = modules.CKEditorError;
+	} );
+
+	var collection;
+
+	beforeEach( () => {
+		collection = new Collection();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'allows to change the id property used by the collection', () => {
+			var item1 = { id: 'foo', name: 'xx' };
+			var item2 = { id: 'foo', name: 'yy' };
+			var collection = new Collection( { idProperty: 'name' } );
+
+			collection.add( item1 );
+			collection.add( item2 );
+
+			expect( collection ).to.have.length( 2 );
+
+			expect( collection.get( 'xx' ) ).to.equal( item1 );
+			expect( collection.remove( 'yy' ) ).to.equal( item2 );
+		} );
+	} );
+
+	describe( 'add', () => {
+		it( 'should be chainable', () => {
+			expect( collection.add( {} ) ).to.equal( collection );
+		} );
+
+		it( 'should change the length', () => {
+			expect( collection ).to.have.length( 0 );
+
+			collection.add( {} );
+			expect( collection ).to.have.length( 1 );
+
+			collection.add( {} );
+			expect( collection ).to.have.length( 2 );
+		} );
+
+		it( 'should enable get( index )', () => {
+			var item1 = {};
+			var item2 = {};
+
+			collection.add( item1 );
+			expect( collection.get( 0 ) ).to.equal( item1 );
+
+			collection.add( item2 );
+			expect( collection.get( 0 ) ).to.equal( item1 );
+			expect( collection.get( 1 ) ).to.equal( item2 );
+		} );
+
+		it( 'should enable get( id )', () => {
+			var item1 = getItem( 'foo' );
+			var item2 = getItem( 'bar' );
+
+			collection.add( item1 );
+			collection.add( item2 );
+
+			expect( collection.get( 'foo' ) ).to.equal( item1 );
+			expect( collection.get( 'bar' ) ).to.equal( item2 );
+		} );
+
+		it( 'should enable get( id ) - custom id property', () => {
+			var collection = new Collection( { idProperty: 'name' } );
+			var item1 = getItem( 'foo', 'name' );
+			var item2 = getItem( 'bar', 'name' );
+
+			collection.add( item1 );
+			collection.add( item2 );
+
+			expect( collection.get( 'foo' ) ).to.equal( item1 );
+			expect( collection.get( 'bar' ) ).to.equal( item2 );
+		} );
+
+		it( 'should generate an id when not defined', () => {
+			var item = {};
+
+			collection.add( item );
+
+			expect( item.id ).to.be.a( 'string' );
+			expect( collection.get( item.id ) ).to.equal( item );
+		} );
+
+		it( 'should generate an id when not defined - custom id property', () => {
+			var collection = new Collection( { idProperty: 'name' } );
+			var item = {};
+
+			collection.add( item );
+
+			expect( item.name ).to.be.a( 'string' );
+			expect( collection.get( item.name ) ).to.equal( item );
+		} );
+
+		it( 'should not change an existing id of an item', () => {
+			var item = getItem( 'foo' );
+
+			collection.add( item );
+
+			expect( item.id ).to.equal( 'foo' );
+		} );
+
+		it( 'should throw when item with this id already exists', () => {
+			var item1 = getItem( 'foo' );
+			var item2 = getItem( 'foo' );
+
+			collection.add( item1 );
+
+			expect( () => {
+				collection.add( item2 );
+			} ).to.throw( CKEditorError, /^collection-add-item-already-exists/ );
+		} );
+
+		it( 'should throw when item\'s id is not a string', () => {
+			var item = { id: 1 };
+
+			expect( () => {
+				collection.add( item );
+			} ).to.throw( CKEditorError, /^collection-add-invalid-id/ );
+		} );
+
+		it(
+			'should not override item under an existing id in case of a collision ' +
+			'between existing items and one with an automatically generated id',
+			() => {
+				collection.add( getItem( '0' ) );
+				collection.add( getItem( '1' ) );
+				collection.add( getItem( '2' ) );
+
+				var item = {};
+
+				collection.add( item );
+
+				expect( item ).to.have.property( 'id', '3' );
+			}
+		);
+
+		it( 'should fire the "add" event', () => {
+			var spy = sinon.spy();
+			var item = {};
+
+			collection.on( 'add', spy );
+
+			collection.add( item );
+
+			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item );
+		} );
+	} );
+
+	describe( 'get', () => {
+		it( 'should return an item', () => {
+			var item = getItem( 'foo' );
+			collection.add( item );
+
+			expect( collection.get( 'foo' ) ).to.equal( item );
+		} );
+
+		it( 'should return null if id does not exist', () => {
+			collection.add( getItem( 'foo' ) );
+
+			expect( collection.get( 'bar' ) ).to.be.null;
+		} );
+
+		it( 'should throw if neither string or number given', () => {
+			expect( () => {
+				collection.get( true );
+			} ).to.throw( CKEditorError, /^collection-get-invalid-arg/ );
+		} );
+	} );
+
+	describe( 'remove', () => {
+		it( 'should remove the model by index', () => {
+			collection.add( getItem( 'bom' ) );
+			collection.add( getItem( 'foo' ) );
+			collection.add( getItem( 'bar' ) );
+
+			expect( collection ).to.have.length( 3 );
+
+			var removedItem = collection.remove( 1 );
+
+			expect( collection ).to.have.length( 2 );
+			expect( collection.get( 'foo' ) ).to.be.null;
+			expect( collection.get( 1 ) ).to.have.property( 'id', 'bar' );
+			expect( removedItem ).to.have.property( 'id', 'foo' );
+		} );
+
+		it( 'should remove the model by index - custom id property', () => {
+			var collection = new Collection( { idProperty: 'name' } );
+
+			collection.add( getItem( 'foo', 'name' ) );
+
+			var removedItem = collection.remove( 0 );
+
+			expect( collection ).to.have.length( 0 );
+			expect( collection.get( 'foo' ) ).to.be.null;
+			expect( removedItem ).to.have.property( 'name', 'foo' );
+		} );
+
+		it( 'should remove the model by id', () => {
+			collection.add( getItem( 'bom' ) );
+			collection.add( getItem( 'foo' ) );
+			collection.add( getItem( 'bar' ) );
+
+			expect( collection ).to.have.length( 3 );
+
+			var removedItem = collection.remove( 'foo' );
+
+			expect( collection ).to.have.length( 2 );
+			expect( collection.get( 'foo' ) ).to.be.null;
+			expect( collection.get( 1 ) ).to.have.property( 'id', 'bar' );
+			expect( removedItem ).to.have.property( 'id', 'foo' );
+		} );
+
+		it( 'should remove the model by model', () => {
+			var item = getItem( 'foo' );
+
+			collection.add( getItem( 'bom' ) );
+			collection.add( item );
+			collection.add( getItem( 'bar' ) );
+
+			expect( collection ).to.have.length( 3 );
+
+			var removedItem = collection.remove( item );
+
+			expect( collection ).to.have.length( 2 );
+			expect( collection.get( 'foo' ) ).to.be.null;
+			expect( collection.get( 1 ) ).to.have.property( 'id', 'bar' );
+			expect( removedItem ).to.equal( item );
+		} );
+
+		it( 'should remove the model by model - custom id property', () => {
+			var collection = new Collection( null, 'name' );
+			var item = getItem( 'foo', 'name' );
+
+			collection.add( item );
+
+			var removedItem = collection.remove( item );
+
+			expect( collection ).to.have.length( 0 );
+			expect( collection.get( 'foo' ) ).to.be.null;
+			expect( removedItem ).to.have.property( 'name', 'foo' );
+		} );
+
+		it( 'should fire the "remove" event', () => {
+			var item1 = getItem( 'foo' );
+			var item2 = getItem( 'bar' );
+			var item3 = getItem( 'bom' );
+
+			collection.add( item1 );
+			collection.add( item2 );
+			collection.add( item3 );
+
+			var spy = sinon.spy();
+
+			collection.on( 'remove', spy );
+
+			collection.remove( 1 );		// by index
+			collection.remove( item1 );	// by model
+			collection.remove( 'bom' );	// by id
+
+			sinon.assert.calledThrice( spy );
+			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item1 );
+			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item2 );
+			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item3 );
+		} );
+
+		it( 'should throw an error on invalid index', () => {
+			collection.add( getItem( 'foo' ) );
+
+			expect( () => {
+				collection.remove( 1 );
+			} ).to.throw( CKEditorError, /^collection-remove-404/ );
+
+			expect( collection ).to.have.length( 1 );
+		} );
+
+		it( 'should throw an error on invalid id', () => {
+			collection.add( getItem( 'foo' ) );
+
+			expect( () => {
+				collection.remove( 'bar' );
+			} ).to.throw( CKEditorError, /^collection-remove-404/ );
+
+			expect( collection ).to.have.length( 1 );
+		} );
+
+		it( 'should throw an error on invalid model', () => {
+			collection.add( getItem( 'foo' ) );
+
+			expect( () => {
+				collection.remove( getItem( 'bar' ) );
+			} ).to.throw( CKEditorError, /^collection-remove-404/ );
+
+			expect( collection ).to.have.length( 1 );
+		} );
+	} );
+
+	describe( 'map', () => {
+		it( 'uses native map', () => {
+			var spy = bender.sinon.stub( Array.prototype, 'map', () => {
+				return [ 'foo' ];
+			} );
+			var ctx = {};
+
+			var ret = collection.map( callback, ctx );
+
+			sinon.assert.calledWithExactly( spy, callback, ctx );
+			expect( ret ).to.deep.equal( [ 'foo' ], 'ret value was forwarded' );
+
+			function callback() {}
+		} );
+	} );
+
+	describe( 'find', () => {
+		it( 'uses native find', () => {
+			var needl = getItem( 'foo' );
+
+			var spy = bender.sinon.stub( Array.prototype, 'find', () => {
+				return needl;
+			} );
+			var ctx = {};
+
+			var ret = collection.find( callback, ctx );
+
+			sinon.assert.calledWithExactly( spy, callback, ctx );
+			expect( ret ).to.equal( needl, 'ret value was forwarded' );
+
+			function callback() {}
+		} );
+	} );
+
+	describe( 'filter', () => {
+		it( 'uses native filter', () => {
+			var needl = getItem( 'foo' );
+
+			var spy = bender.sinon.stub( Array.prototype, 'filter', () => {
+				return [ needl ];
+			} );
+			var ctx = {};
+
+			var ret = collection.filter( callback, ctx );
+
+			sinon.assert.calledWithExactly( spy, callback, ctx );
+			expect( ret ).to.deep.equal( [ needl ], 'ret value was forwarded' );
+
+			function callback() {}
+		} );
+	} );
+
+	describe( 'iterator', () => {
+		it( 'covers the whole collection', () => {
+			var item1 = getItem( 'foo' );
+			var item2 = getItem( 'bar' );
+			var item3 = getItem( 'bom' );
+			var items = [];
+
+			collection.add( item1 );
+			collection.add( item2 );
+			collection.add( item3 );
+
+			for ( let item of collection ) {
+				items.push( item.id );
+			}
+
+			expect( items ).to.deep.equal( [ 'foo', 'bar', 'bom' ] );
+		} );
+	} );
+} );

+ 0 - 183
packages/ckeditor5-utils/tests/mvc/collection/collection.js

@@ -1,183 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-var modules = bender.amd.require( 'collection', 'model' );
-
-bender.tools.createSinonSandbox();
-
-describe( 'add', function() {
-	it( 'should change the length and enable get', function() {
-		var Model = modules.model;
-
-		var box = getCollection();
-
-		expect( box ).to.have.length( 0 );
-
-		box.add( getItem() );
-
-		expect( box ).to.have.length( 1 );
-
-		expect( box.get( 0 ) ).to.be.an.instanceof( Model );
-	} );
-
-	it( 'should fire the "add" event', function() {
-		var spy = sinon.spy();
-
-		var box = getCollection();
-		box.on( 'add', spy );
-
-		var item = getItem();
-		box.add( item );
-
-		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item );
-	} );
-} );
-
-describe( 'get', function() {
-	it( 'should return null if index does not exist', function() {
-		var box = getCollection();
-		box.add( getItem() );
-
-		expect( box.get( 1 ) ).to.be.null;
-	} );
-} );
-
-describe( 'remove', function() {
-	it( 'should remove the model by index', function() {
-		var box = getCollection();
-		var item = getItem();
-
-		box.add( item );
-
-		expect( box ).to.have.length( 1 );
-
-		box.remove( 0 );
-
-		expect( box ).to.have.length( 0 );
-	} );
-
-	it( 'should remove the model by model', function() {
-		var box = getCollection();
-		var item = getItem();
-
-		box.add( item );
-
-		expect( box ).to.have.length( 1 );
-
-		box.remove( item );
-
-		expect( box ).to.have.length( 0 );
-	} );
-
-	it( 'should fire the "remove" event', function() {
-		var box = getCollection();
-		var item1 = getItem();
-		var item2 = getItem();
-
-		box.add( item1 );
-		box.add( item2 );
-
-		var spy = sinon.spy();
-
-		box.on( 'remove', spy );
-
-		box.remove( 1 );		// by index
-		box.remove( item1 );	// by model
-
-		sinon.assert.calledTwice( spy );
-		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item1 );
-		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item2 );
-	} );
-
-	it( 'should throw an error on invalid index', function() {
-		var CKEditorError = modules.ckeditorerror;
-
-		var box = getCollection();
-		box.add( getItem() );
-
-		expect( function() {
-			box.remove( 1 );
-		} ).to.throw( CKEditorError, /^collection-index-404/ );
-	} );
-
-	it( 'should throw an error on invalid model', function() {
-		var CKEditorError = modules.ckeditorerror;
-
-		var box = getCollection();
-		box.add( getItem() );
-
-		expect( function() {
-			box.remove( getItem() );
-		} ).to.throw( CKEditorError, /^collection-model-404/ );
-	} );
-} );
-
-describe( 'forEach', function() {
-	it( 'uses native forEach', function() {
-		var collection = getCollection();
-		collection.add( getItem() );
-
-		var spy = bender.sinon.spy( Array.prototype, 'forEach' );
-		var ctx = {};
-
-		collection.forEach( callback, ctx );
-
-		sinon.assert.calledWithExactly( spy, callback, ctx );
-
-		function callback() {}
-	} );
-} );
-
-describe( 'find', function() {
-	it( 'uses native find', function() {
-		var collection = getCollection();
-		var needl = getItem();
-
-		var spy = bender.sinon.stub( Array.prototype, 'find', function() {
-			return needl;
-		} );
-		var ctx = {};
-
-		var ret = collection.find( callback, ctx );
-
-		sinon.assert.calledWithExactly( spy, callback, ctx );
-		expect( ret ).to.equal( needl, 'ret value was forwarded' );
-
-		function callback() {}
-	} );
-} );
-
-describe( 'filter', function() {
-	it( 'uses native filter', function() {
-		var collection = getCollection();
-		var needl = getItem();
-
-		var spy = bender.sinon.stub( Array.prototype, 'filter', function() {
-			return needl;
-		} );
-		var ctx = {};
-
-		var ret = collection.filter( callback, ctx );
-
-		sinon.assert.calledWithExactly( spy, callback, ctx );
-		expect( ret ).to.equal( needl, 'ret value was forwarded' );
-
-		function callback() {}
-	} );
-} );
-
-function getCollection() {
-	var Collection = modules.collection;
-
-	return new Collection();
-}
-
-function getItem() {
-	var Model = modules.model;
-
-	return new Model();
-}

+ 0 - 254
packages/ckeditor5-utils/tests/mvc/collection/namedcollection.js

@@ -1,254 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-var modules = bender.amd.require( 'namedcollection', 'model', 'ckeditorerror' );
-
-describe( 'add', function() {
-	it( 'changes the length and enables get', function() {
-		var box = getCollection();
-
-		expect( box ).to.have.length( 0 );
-
-		var item = getItem( 'foo' );
-		box.add( item );
-
-		expect( box ).to.have.length( 1 );
-
-		expect( box.get( 'foo' ) ).to.equal( item );
-	} );
-
-	it( 'fires the "add" event', function() {
-		var spy = sinon.spy();
-
-		var box = getCollection();
-		box.on( 'add', spy );
-
-		var item = getItem( 'foo' );
-		box.add( item );
-
-		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item );
-	} );
-
-	it( 'throws an error if model is not named', function() {
-		var Model = modules.model;
-		var CKEditorError = modules.ckeditorerror;
-		var box = getCollection();
-
-		expect( box ).to.have.length( 0 );
-
-		var item = new Model();
-
-		expect( function() {
-			box.add( item );
-		} ).to.throw( CKEditorError, /^namedcollection-add/ );
-	} );
-
-	it( 'throws an error if some model already exists under the same name', function() {
-		var CKEditorError = modules.ckeditorerror;
-		var box = getCollection();
-
-		expect( box ).to.have.length( 0 );
-
-		box.add( getItem( 'foo' ) );
-
-		expect( function() {
-			box.add( getItem( 'foo' ) );
-		} ).to.throw( CKEditorError, /^namedcollection-add/ );
-	} );
-} );
-
-describe( 'get', function() {
-	it( 'should throw an error on invalid name', function() {
-		var box = getCollection();
-		box.add( getItem( 'foo' ) );
-
-		expect( box.get( 'bar' ) ).to.be.null;
-	} );
-} );
-
-describe( 'remove', function() {
-	it( 'should remove the model by name', function() {
-		var box = getCollection();
-		var item = getItem( 'foo' );
-
-		box.add( item );
-
-		expect( box ).to.have.length( 1 );
-
-		box.remove( 'foo' );
-
-		expect( box ).to.have.length( 0 );
-	} );
-
-	it( 'should remove the model by model', function() {
-		var box = getCollection();
-		var item = getItem( 'foo' );
-
-		box.add( item );
-
-		expect( box ).to.have.length( 1 );
-
-		box.remove( item );
-
-		expect( box ).to.have.length( 0 );
-	} );
-
-	it( 'should fire the "remove" event', function() {
-		var box = getCollection();
-		var item1 = getItem( 'foo' );
-		var item2 = getItem( 'bar' );
-
-		box.add( item1 );
-		box.add( item2 );
-
-		var spy = sinon.spy();
-
-		box.on( 'remove', spy );
-
-		box.remove( 'foo' ); // by name
-		box.remove( item2 ); // by model
-
-		sinon.assert.calledTwice( spy );
-		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item1 );
-		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item2 );
-	} );
-
-	it( 'should throw an error if model is not named', function() {
-		var CKEditorError = modules.ckeditorerror;
-		var Model = modules.model;
-		var box = getCollection();
-
-		expect( function() {
-			box.remove( new Model() );
-		} ).to.throw( CKEditorError, /^namedcollection-remove/ );
-	} );
-
-	it( 'should throw an error if model does not exist (by name)', function() {
-		var CKEditorError = modules.ckeditorerror;
-		var box = getCollection();
-
-		expect( function() {
-			box.remove( 'foo' );
-		} ).to.throw( CKEditorError, /^namedcollection-remove/ );
-	} );
-
-	it( 'should throw an error if model does not exist (by model)', function() {
-		var CKEditorError = modules.ckeditorerror;
-		var box = getCollection();
-
-		expect( function() {
-			box.remove( getItem( 'foo' ) );
-		} ).to.throw( CKEditorError, /^namedcollection-remove/ );
-	} );
-
-	it( 'should throw an error if model does not exist (by model)', function() {
-		var CKEditorError = modules.ckeditorerror;
-		var box = getCollection();
-
-		expect( function() {
-			box.remove( getItem( 'foo' ) );
-		} ).to.throw( CKEditorError, /^namedcollection-remove/ );
-	} );
-
-	it( 'should throw an error if a different model exists under the same name', function() {
-		var CKEditorError = modules.ckeditorerror;
-		var box = getCollection();
-		var item = getItem( 'foo' );
-
-		box.add( item );
-
-		expect( function() {
-			box.remove( getItem( 'foo' ) );
-		} ).to.throw( CKEditorError, /^namedcollection-remove/ );
-	} );
-} );
-
-describe( 'forEach', function() {
-	it( 'executes callback for each item', function() {
-		var collection = getCollection();
-		collection.add( getItem( 'foo' ) );
-		collection.add( getItem( 'bar' ) );
-
-		var ctx = {};
-		var models = [];
-		var names = [];
-
-		collection.forEach( callback, ctx );
-		expect( models.sort() ).deep.equals( [ 'bar', 'foo' ] );
-		expect( names.sort() ).deep.equals( [ 'bar', 'foo' ] );
-
-		function callback( model, name ) {
-			expect( this ).to.equal( ctx ); /* jshint ignore:line */
-			models.push( model.name );
-			names.push( name );
-		}
-	} );
-} );
-
-describe( 'find', function() {
-	it( 'finds the right item', function() {
-		var Model = modules.model;
-
-		var collection = getCollection();
-		var needl = getItem( 'foo' );
-		collection.add( getItem( 'bar' ) );
-		collection.add( needl );
-		collection.add( getItem( 'bom' ) );
-
-		var ctx = {};
-
-		var ret = collection.find( callback, ctx );
-		expect( ret ).to.equal( needl );
-
-		function callback( model, name ) {
-			expect( this ).to.equal( ctx ); /* jshint ignore:line */
-			expect( model ).is.an.instanceof( Model );
-			expect( name ).to.be.a( 'string' );
-
-			return model.name == 'foo';
-		}
-	} );
-} );
-
-describe( 'filter', function() {
-	it( 'finds the right items', function() {
-		var Model = modules.model;
-
-		var collection = getCollection();
-		collection.add( getItem( 'bar' ) );
-		collection.add( getItem( 'foo' ) );
-		collection.add( getItem( 'bom' ) );
-
-		var ctx = {};
-
-		var ret = collection.filter( callback, ctx );
-		expect( ret ).to.have.keys( [ 'bar', 'bom' ] );
-
-		function callback( model, name ) {
-			expect( this ).to.equal( ctx ); /* jshint ignore:line */
-			expect( model ).is.an.instanceof( Model );
-			expect( name ).to.be.a( 'string' );
-
-			return model.name[ 0 ] == 'b';
-		}
-	} );
-} );
-
-function getCollection() {
-	var NamedCollection = modules.namedcollection;
-
-	return new NamedCollection();
-}
-
-function getItem( name ) {
-	var Model = modules.model;
-
-	var model = new Model();
-	model.name = name;
-
-	return model;
-}