Explorar o código

Merge pull request #19 from cksource/t/16

t/16: Introduced the configuration API
Grzegorz Pabian %!s(int64=10) %!d(string=hai) anos
pai
achega
2fc405722d

+ 9 - 3
packages/ckeditor5-engine/src/ckeditor.js

@@ -14,7 +14,7 @@
  * @singleton
  */
 
-CKEDITOR.define( [ 'editor', 'mvc/collection', 'promise' ], function( Editor, Collection, Promise ) {
+CKEDITOR.define( [ 'editor', 'mvc/collection', 'promise', 'config' ], function( Editor, Collection, Promise, Config ) {
 	var CKEDITOR = {
 		/**
 		 * A collection containing all editor instances created.
@@ -40,7 +40,7 @@ CKEDITOR.define( [ 'editor', 'mvc/collection', 'promise' ], function( Editor, Co
 		 * created instance.
 		 * @returns {Promise} A promise, which will be fulfilled with the created editor.
 		 */
-		create: function( element ) {
+		create: function( element, config ) {
 			var that = this;
 
 			return new Promise( function( resolve, reject ) {
@@ -53,7 +53,7 @@ CKEDITOR.define( [ 'editor', 'mvc/collection', 'promise' ], function( Editor, Co
 					}
 				}
 
-				var editor = new Editor( element );
+				var editor = new Editor( element, config );
 
 				that.instances.add( editor );
 
@@ -66,6 +66,12 @@ CKEDITOR.define( [ 'editor', 'mvc/collection', 'promise' ], function( Editor, Co
 			} );
 		},
 
+		/**
+		 * Holds global configuration defaults, which will be used by editor instances when such configurations are not
+		 * available on them directly.
+		 */
+		config: new Config(),
+
 		/**
 		 * Gets the full URL path for the specified plugin.
 		 *

+ 200 - 0
packages/ckeditor5-engine/src/config.js

@@ -0,0 +1,200 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * Handles a configuration dictionary.
+ *
+ * @class Config
+ * @extends Model
+ */
+
+CKEDITOR.define( [ 'mvc/model', 'utils' ], function( Model, utils ) {
+	var Config = Model.extend( {
+		/**
+		 * Creates an instance of the {@link Config} class.
+		 *
+		 * @param {Object} [configurations] The initial configurations to be set.
+		 * @constructor
+		 */
+		constructor: function Config( configurations ) {
+			// Call super-constructor.
+			Model.apply( this );
+
+			if ( configurations ) {
+				this.set( configurations );
+			}
+		},
+
+		/**
+		 * Set configuration values.
+		 *
+		 * It accepts both a name/value pair or an object, which properties and values will be used to set
+		 * configurations.
+		 *
+		 * It also accepts setting a "deep configuration" by using dots in the name. For example, `'resize.width'` sets
+		 * the value for the `width` configuration in the `resize` subset.
+		 *
+		 *     config.set( 'width', 500 );
+		 *     config.set( 'toolbar.collapsed', true );
+		 *
+		 *     // Equivalent to:
+		 *     config.set( {
+		 *         width: 500
+		 *         toolbar: {
+		 *             collapsed: true
+		 *         }
+		 *     } );
+		 *
+		 * Passing an object as the value will amend the configuration, not replace it.
+		 *
+		 *     config.set( 'toolbar', {
+		 *         collapsed: true,
+		 *     } );
+		 *
+		 *     config.set( 'toolbar', {
+		 *         color: 'red',
+		 *     } );
+		 *
+		 *     config.toolbar.collapsed; // true
+		 *     config.toolbar.color; // 'red'
+		 *
+		 * @param {String|Object} nameOrConfigurations The configuration name or an object from which take properties as
+		 * configuration entries. Configuration names are case-insensitive.
+		 * @param {*} [value=null] The configuration value. Used if a name is passed to nameOrConfigurations.
+		 */
+		set: function( name, value ) {
+			// Just pass the call to the original set() in case of an object. It'll deal with recursing through the
+			// object and calling set( name, value ) again for each property.
+			if ( utils.isObject( name ) ) {
+				Model.prototype.set.apply( this, arguments );
+
+				return;
+			}
+
+			// The target for this configuration is, for now, this object.
+			//jscs:disable safeContextKeyword
+			var target = this;
+			//jscs:enable
+
+			// The configuration name should be split into parts if it has dots. E.g: `resize.width`.
+			var parts = name.toLowerCase().split( '.' );
+
+			// Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`
+			name = parts.pop();
+
+			// Retrieves the final target for this configuration recursively.
+			for ( var i = 0; i < parts.length; i++ ) {
+				// The target will always be an instance of Config.
+				if ( !( target[ parts[ i ] ] instanceof Config ) ) {
+					target.set( parts[ i ], new Config() );
+				}
+
+				target = target[ parts[ i ] ];
+			}
+
+			// Values set as pure objects will be treated as Config subsets.
+			if ( utils.isPlainObject( value ) ) {
+				// If the target is an instance of Config (a deep config subset).
+				if ( target[ name ] instanceof Config ) {
+					// Amend the target with the value, instead of replacing it.
+					target[ name ].set( value );
+
+					return;
+				}
+
+				value = new Config( value );
+			}
+
+			// Values will never be undefined.
+			if ( typeof value == 'undefined' ) {
+				value = null;
+			}
+
+			// Call the original set() on the target.
+			Model.prototype.set.call( target, name, value );
+		},
+
+		/**
+		 * Gets the value for a configuration entry.
+		 *
+		 *     config.get( 'name' );
+		 *
+		 * Deep configurations can be retrieved by separating each part with a dot.
+		 *
+		 *     config.get( 'toolbar.collapsed' );
+		 *
+		 * @param {String} name The configuration name. Configuration names are case-insensitive.
+		 * @returns {*} The configuration value or `undefined` if the configuration entry was not found.
+		 */
+		get: function( name ) {
+			// The target for this configuration is, for now, this object.
+			//jscs:disable safeContextKeyword
+			var source = this;
+			//jscs:enable
+
+			// The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`]
+			var parts = name.toLowerCase().split( '.' );
+
+			// Take the name of the configuration from the parts. E.g. `resize.width` -> `width`
+			name = parts.pop();
+
+			// Retrieves the source for this configuration recursively.
+			for ( var i = 0; i < parts.length; i++ ) {
+				// The target will always be an instance of Config.
+				if ( !( source[ parts[ i ] ] instanceof Config ) ) {
+					source = null;
+					break;
+				}
+
+				source = source[ parts[ i ] ];
+			}
+
+			// Try to retrieve it from the source object.
+			if ( source && ( typeof source[ name ] != 'undefined' ) ) {
+				return source[ name ];
+			}
+
+			// If not found, take it from the definition.
+			if ( this.definition ) {
+				return this.definition[ name ];
+			}
+
+			return undefined;
+		},
+
+		/**
+		 * Defines the name and default value for configurations. It accepts the same parameters as the
+		 * {@link Config#set set()} method.
+		 *
+		 * On first call, the {@link Config#definition definition} property is created to hold all defined
+		 * configurations.
+		 *
+		 * This method is supposed to be called by plugin developers to setup plugin's configurations. It would be
+		 * rarely used for other needs.
+		 *
+		 * @param {String|Object} nameOrConfigurations The configuration name or an object from which take properties as
+		 * configuration entries.
+		 * @param {*} [value] The configuration value. Used if a name is passed to nameOrConfigurations. If undefined,
+		 * the configuration is set to `null`.
+		 */
+		define: function( name, value ) {
+			if ( !this.definition ) {
+				/**
+				 *
+				 *
+				 * @property
+				 * @type {Config}
+				 */
+				this.definition = new Config();
+			}
+
+			this.definition.set( name, value );
+		}
+	} );
+
+	return Config;
+} );

+ 13 - 2
packages/ckeditor5-engine/src/editor.js

@@ -11,7 +11,7 @@
  * @class Editor
  */
 
-CKEDITOR.define( [ 'ckeditor', 'mvc/model', 'utils' ], function( CKEDITOR, Model ) {
+CKEDITOR.define( [ 'mvc/model', 'editorconfig' ], function( Model, EditorConfig ) {
 	var Editor = Model.extend( {
 		/**
 		 * Creates a new instance of the Editor class.
@@ -22,7 +22,7 @@ CKEDITOR.define( [ 'ckeditor', 'mvc/model', 'utils' ], function( CKEDITOR, Model
 		 * @param {HTMLElement} element The DOM element that will be the source for the created editor.
 		 * @constructor
 		 */
-		constructor: function Editor( element ) {
+		constructor: function Editor( element, config ) {
 			/**
 			 * The original host page element upon which the editor is created. It is only supposed to be provided on
 			 * editor creation and is not subject to be modified.
@@ -31,6 +31,17 @@ CKEDITOR.define( [ 'ckeditor', 'mvc/model', 'utils' ], function( CKEDITOR, Model
 			 * @property {HTMLElement}
 			 */
 			this.element = element;
+
+			/**
+			 * Holds all configurations specific to this editor instance.
+			 *
+			 * This instance of the {@link Config} class is customized so its {@link Config#get} method will retrieve
+			 * global configurations available in {@link CKEDITOR.config} if configurations are not found in the
+			 * instance itself.
+			 *
+			 * @type {Config}
+			 */
+			this.config = new EditorConfig( config );
 		},
 
 		/**

+ 55 - 0
packages/ckeditor5-engine/src/editorconfig.js

@@ -0,0 +1,55 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * Handles a configuration dictionary for an editor instance.
+ *
+ * The basic difference between {@link EditorConfig} and {@link Config} is that {@link EditorConfig#get} retrieves
+ * configurations from {@link CKEDITOR#config} if they are not found.
+ *
+ * @class EditorConfig
+ * @extends Config
+ */
+
+CKEDITOR.define( [ 'ckeditor', 'config' ], function( CKE, Config ) {
+	var EditorConfig = Config.extend( {
+		/**
+		 * Creates an instance of the {@link EditorConfig} class.
+		 *
+		 * @param {Object} [configurations] The initial configurations to be set.
+		 * @constructor
+		 */
+		constructor: function EditorConfig() {
+			// Call super-constructor.
+			Config.apply( this, arguments );
+		},
+
+		/**
+		 * @inheritdoc Config#get
+		 */
+		get: function() {
+			// Try to take it from this editor instance.
+			var value = Config.prototype.get.apply( this, arguments );
+
+			// If the configuration is not defined in the instance, try to take it from CKEDITOR.config.
+			if ( typeof value == 'undefined' ) {
+				// There is a circular dependency issue here: CKEDITOR -> Editor -> EditorConfig -> CKEDITOR.
+				// Therefore we need to require() it again here. That's why the parameter was named CKE.
+				//
+				// Note additionally that we still keep 'ckeditor' in the dependency list for correctness, to ensure
+				// that the module is loaded.
+
+				CKE = CKE || CKEDITOR.require( 'ckeditor' );
+				value = CKE.config.get.apply( CKE.config, arguments );
+			}
+
+			return value;
+		}
+	} );
+
+	return EditorConfig;
+} );

+ 110 - 1
packages/ckeditor5-engine/src/lib/lodash/lodash-ckeditor.js

@@ -1,7 +1,7 @@
 /**
  * @license
  * Lo-Dash 2.4.1 (Custom Build) <http://lodash.com/>
- * Build: `lodash modern exports="amd" include="clone,extend,isObject" --debug --output src/lib/lodash/lodash-ckeditor.js`
+ * Build: `lodash modern exports="amd" include="clone,extend,isPlainObject,isObject" --debug --output src/lib/lodash/lodash-ckeditor.js`
  * Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
  * Based on Underscore.js 1.5.2 <http://underscorejs.org/LICENSE>
  * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
@@ -148,6 +148,7 @@
 
   /** Native method shortcuts */
   var fnToString = Function.prototype.toString,
+      getPrototypeOf = isNative(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf,
       hasOwnProperty = objectProto.hasOwnProperty,
       push = arrayRef.push,
       unshift = arrayRef.unshift;
@@ -646,6 +647,34 @@
     defineProperty(func, '__bindData__', descriptor);
   };
 
+  /**
+   * A fallback implementation of `isPlainObject` which checks if a given value
+   * is an object created by the `Object` constructor, assuming objects created
+   * by the `Object` constructor have no inherited enumerable properties and that
+   * there are no `Object.prototype` extensions.
+   *
+   * @private
+   * @param {*} value The value to check.
+   * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+   */
+  function shimIsPlainObject(value) {
+    var ctor,
+        result;
+
+    // avoid non Object objects, `arguments` objects, and DOM elements
+    if (!(value && toString.call(value) == objectClass) ||
+        (ctor = value.constructor, isFunction(ctor) && !(ctor instanceof ctor))) {
+      return false;
+    }
+    // In most environments an object's own properties are iterated before
+    // its inherited properties. If the last iterated property is an object's
+    // own property then there are no inherited enumerable properties.
+    forIn(value, function(value, key) {
+      result = key;
+    });
+    return typeof result == 'undefined' || hasOwnProperty.call(value, result);
+  }
+
   /*--------------------------------------------------------------------------*/
 
   /**
@@ -821,6 +850,48 @@
     return baseClone(value, isDeep, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1));
   }
 
+  /**
+   * Iterates over own and inherited enumerable properties of an object,
+   * executing the callback for each property. The callback is bound to `thisArg`
+   * and invoked with three arguments; (value, key, object). Callbacks may exit
+   * iteration early by explicitly returning `false`.
+   *
+   * @static
+   * @memberOf _
+   * @type Function
+   * @category Objects
+   * @param {Object} object The object to iterate over.
+   * @param {Function} [callback=identity] The function called per iteration.
+   * @param {*} [thisArg] The `this` binding of `callback`.
+   * @returns {Object} Returns `object`.
+   * @example
+   *
+   * function Shape() {
+   *   this.x = 0;
+   *   this.y = 0;
+   * }
+   *
+   * Shape.prototype.move = function(x, y) {
+   *   this.x += x;
+   *   this.y += y;
+   * };
+   *
+   * _.forIn(new Shape, function(value, key) {
+   *   console.log(key);
+   * });
+   * // => logs 'x', 'y', and 'move' (property order is not guaranteed across environments)
+   */
+  var forIn = function(collection, callback, thisArg) {
+    var index, iterable = collection, result = iterable;
+    if (!iterable) return result;
+    if (!objectTypes[typeof iterable]) return result;
+    callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3);
+      for (index in iterable) {
+        if (callback(iterable[index], index, collection) === false) return result;
+      }
+    return result
+  };
+
   /**
    * Iterates over own enumerable properties of an object, executing the callback
    * for each property. The callback is bound to `thisArg` and invoked with three
@@ -903,6 +974,42 @@
     return !!(value && objectTypes[typeof value]);
   }
 
+  /**
+   * Checks if `value` is an object created by the `Object` constructor.
+   *
+   * @static
+   * @memberOf _
+   * @category Objects
+   * @param {*} value The value to check.
+   * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+   * @example
+   *
+   * function Shape() {
+   *   this.x = 0;
+   *   this.y = 0;
+   * }
+   *
+   * _.isPlainObject(new Shape);
+   * // => false
+   *
+   * _.isPlainObject([1, 2, 3]);
+   * // => false
+   *
+   * _.isPlainObject({ 'x': 0, 'y': 0 });
+   * // => true
+   */
+  var isPlainObject = !getPrototypeOf ? shimIsPlainObject : function(value) {
+    if (!(value && toString.call(value) == objectClass)) {
+      return false;
+    }
+    var valueOf = value.valueOf,
+        objProto = isNative(valueOf) && (objProto = getPrototypeOf(valueOf)) && getPrototypeOf(objProto);
+
+    return objProto
+      ? (value == objProto || getPrototypeOf(value) == objProto)
+      : shimIsPlainObject(value);
+  };
+
   /*--------------------------------------------------------------------------*/
 
   /**
@@ -1019,6 +1126,7 @@
   lodash.assign = assign;
   lodash.bind = bind;
   lodash.forEach = forEach;
+  lodash.forIn = forIn;
   lodash.forOwn = forOwn;
   lodash.keys = keys;
 
@@ -1033,6 +1141,7 @@
   lodash.isArray = isArray;
   lodash.isFunction = isFunction;
   lodash.isObject = isObject;
+  lodash.isPlainObject = isPlainObject;
   lodash.noop = noop;
 
   /*--------------------------------------------------------------------------*/

+ 8 - 0
packages/ckeditor5-engine/src/utils-lodash.js

@@ -33,6 +33,14 @@
 		 */
 		'extend',
 
+		/**
+		 * See Lo-Dash: https://lodash.com/docs#isPlainObject
+		 *
+		 * @member utils
+		 * @method isPlainObject
+		 */
+		'isPlainObject',
+
 		/**
 		 * See Lo-Dash: https://lodash.com/docs#isObject
 		 *

+ 18 - 1
packages/ckeditor5-engine/tests/ckeditor/ckeditor.js

@@ -7,7 +7,7 @@
 
 'use strict';
 
-var modules = bender.amd.require( 'ckeditor', 'editor', 'promise' );
+var modules = bender.amd.require( 'ckeditor', 'editor', 'promise', 'config' );
 
 var content = document.getElementById( 'content' );
 
@@ -48,6 +48,14 @@ describe( 'create', function() {
 		} );
 	} );
 
+	it( 'should set configurations on the new editor', function() {
+		var CKEDITOR = modules.ckeditor;
+
+		return CKEDITOR.create( content, { test: 1 } ).then( function( editor ) {
+			expect( editor.config.test ).to.equals( 1 );
+		} );
+	} );
+
 	it( 'should add the editor to the `instances` collection', function() {
 		var CKEDITOR = modules.ckeditor;
 
@@ -95,3 +103,12 @@ describe( 'create', function() {
 		} );
 	} );
 } );
+
+describe( 'config', function() {
+	it( 'should be an instance of Config', function() {
+		var CKEDITOR = modules.ckeditor;
+		var Config = modules.config;
+
+		expect( CKEDITOR.config ).to.be.an.instanceof( Config );
+	} );
+} );

+ 252 - 0
packages/ckeditor5-engine/tests/config/config.js

@@ -0,0 +1,252 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals describe, it, expect, beforeEach */
+
+'use strict';
+
+var modules = bender.amd.require( 'config' );
+
+var config;
+
+beforeEach( function() {
+	var Config = modules.config;
+
+	config = new Config( {
+		creator: 'inline',
+		language: 'pl',
+		resize: {
+			minHeight: 300,
+			maxHeight: 800,
+			icon: {
+				path: 'xyz'
+			}
+		},
+		toolbar: 'top'
+	} );
+} );
+
+describe( 'constructor', function() {
+	it( 'should set configurations', function() {
+		expect( config ).to.have.property( 'creator' ).to.equals( 'inline' );
+		expect( config ).to.have.property( 'language' ).to.equals( 'pl' );
+		expect( config ).to.have.property( 'resize' ).to.have.property( 'minheight' ).to.equals( 300 );
+		expect( config ).to.have.property( 'resize' ).to.have.property( 'maxheight' ).to.equals( 800 );
+		expect( config ).to.have.property( 'resize' ).to.have.property( 'icon' )
+			.to.have.property( 'path' ).to.equals( 'xyz' );
+		expect( config ).to.have.property( 'toolbar' ).to.equals( 'top' );
+	} );
+
+	it( 'should work with no parameters', function() {
+		var Config = modules.config;
+
+		// No error should be thrown.
+		config = new Config();
+	} );
+} );
+
+describe( 'set', function() {
+	it( 'should create Config instances for objects', function() {
+		var Config = modules.config;
+
+		expect( config.resize ).to.be.an.instanceof( Config );
+		expect( config.resize.icon ).to.be.an.instanceof( Config );
+	} );
+
+	it( 'should set configurations when passing objects', function() {
+		config.set( {
+			option1: 1,
+			option2: {
+				subOption21: 21
+			}
+		} );
+
+		expect( config )
+			.to.have.property( 'option1' ).to.equals( 1 );
+
+		expect( config )
+			.to.have.property( 'option2' )
+			.to.have.property( 'suboption21' ).to.equals( 21 );
+	} );
+
+	it( 'should set configurations when passing name and value', function() {
+		config.set( 'something', 'anything' );
+
+		expect( config ).to.have.property( 'something' ).to.equals( 'anything' );
+	} );
+
+	it( 'should set configurations when passing name.with.deep and value', function() {
+		config.set( 'color.red', 'f00' );
+		config.set( 'background.color.blue', '00f' );
+
+		expect( config )
+			.to.have.property( 'color' )
+			.to.have.property( 'red' ).to.equals( 'f00' );
+
+		expect( config )
+			.to.have.property( 'background' )
+			.to.have.property( 'color' )
+			.to.have.property( 'blue' ).to.equals( '00f' );
+	} );
+
+	it( 'should override and expand deep configurations', function() {
+		config.set( {
+			resize: {
+				minHeight: 400,		// Override
+				hidden: true,		// Expand
+				icon: {
+					path: 'abc',	// Override
+					url: true		// Expand
+				}
+			}
+		} );
+
+		expect( config ).to.have.property( 'resize' );
+		expect( config.resize ).to.have.property( 'minheight' ).to.equals( 400 );
+		expect( config.resize ).to.have.property( 'maxheight' ).to.equals( 800 );	// Not touched
+		expect( config.resize ).to.have.property( 'hidden' ).to.equals( true );
+
+		expect( config.resize ).to.have.property( 'icon' );
+		expect( config.resize.icon ).to.have.property( 'path' ).to.equals( 'abc' );
+		expect( config.resize.icon ).to.have.property( 'url' ).to.equals( true );
+	} );
+
+	it( 'should replace a simple entry with a Config instance', function() {
+		var Config = modules.config;
+
+		config.set( 'test', 1 );
+		config.set( 'test', {
+			prop: 1
+		} );
+
+		expect( config.test ).to.be.an.instanceof( Config );
+		expect( config.test.prop ).to.equals( 1 );
+	} );
+
+	it( 'should replace a simple entry with a Config instance when passing an object', function() {
+		var Config = modules.config;
+
+		config.set( 'test', 1 );
+		config.set( {
+			test: {
+				prop: 1
+			}
+		} );
+
+		expect( config.test ).to.be.an.instanceof( Config );
+		expect( config.test.prop ).to.equals( 1 );
+	} );
+
+	it( 'should replace a simple entry with a Config instance when passing a name.with.deep', function() {
+		var Config = modules.config;
+
+		config.set( 'test.prop', 1 );
+		config.set( 'test.prop.value', 1 );
+
+		expect( config.test ).to.be.an.instanceof( Config );
+		expect( config.test.prop ).to.be.an.instanceof( Config );
+		expect( config.test.prop.value ).to.equals( 1 );
+	} );
+
+	it( 'should not create Config instances for non-pure objects', function() {
+		function SomeClass() {}
+
+		config.set( 'date', new Date() );
+		config.set( {
+			instance: new SomeClass()
+		} );
+
+		expect( config.date ).to.be.an.instanceof( Date );
+		expect( config.instance ).to.be.an.instanceof( SomeClass );
+	} );
+
+	it( 'should set `null` for undefined value', function() {
+		config.set( 'test' );
+
+		expect( config.test ).to.be.null();
+		expect( config.get( 'test' ) ).to.be.null();
+	} );
+} );
+
+describe( 'get', function() {
+	it( 'should retrieve a configuration', function() {
+		expect( config.get( 'creator' ) ).to.equals( 'inline' );
+	} );
+
+	it( 'should retrieve a deep configuration', function() {
+		expect( config.get( 'resize.minheight' ) ).to.equals( 300 );
+		expect( config.get( 'resize.icon.path' ) ).to.equals( 'xyz' );
+	} );
+
+	it( 'should retrieve a subset of the configuration', function() {
+		var resizeConfig = config.get( 'resize' );
+
+		expect( resizeConfig ).to.have.property( 'minheight' ).to.equals( 300 );
+		expect( resizeConfig ).to.have.property( 'maxheight' ).to.equals( 800 );
+		expect( resizeConfig ).to.have.property( 'icon' ).to.have.property( 'path' ).to.equals( 'xyz' );
+
+		var iconConfig = resizeConfig.get( 'icon' );
+
+		expect( iconConfig ).to.have.property( 'path' ).to.equals( 'xyz' );
+	} );
+
+	it( 'should retrieve values case-insensitively', function() {
+		expect( config.get( 'Creator' ) ).to.equals( 'inline' );
+		expect( config.get( 'CREATOR' ) ).to.equals( 'inline' );
+		expect( config.get( 'resize.minHeight' ) ).to.equals( 300 );
+		expect( config.get( 'resize.MINHEIGHT' ) ).to.equals( 300 );
+	} );
+
+	it( 'should return undefined for non existing configuration', function() {
+		expect( config.get( 'invalid' ) ).to.be.undefined();
+	} );
+
+	it( 'should return undefined for non existing deep configuration', function() {
+		expect( config.get( 'resize.invalid.value' ) ).to.be.undefined();
+	} );
+} );
+
+describe( 'define', function() {
+	it( 'should create the definition property', function() {
+		expect( config ).to.not.have.property( 'definition' );
+
+		config.define( 'test', 1 );
+
+		expect( config ).to.have.property( 'definition' );
+	} );
+
+	it( 'should set configurations in the definition property', function() {
+		config.define( 'test1', 1 );
+
+		// This is for Code Coverage to ensure that it works when `definition` is already defined.
+		config.define( 'test2', 2 );
+
+		expect( config.definition ).to.have.property( 'test1' ).to.equals( 1 );
+		expect( config.definition ).to.have.property( 'test2' ).to.equals( 2 );
+	} );
+
+	it( 'should set configurations passed as object in the definition property', function() {
+		config.define( {
+			test: 1
+		} );
+
+		expect( config.definition ).to.have.property( 'test' ).to.equals( 1 );
+	} );
+
+	it( 'should not define main config properties but still be retrieved with get()', function() {
+		config.define( 'test', 1 );
+
+		expect( config ).to.not.have.property( 'test' );
+		expect( config.get( 'test' ) ).to.equals( 1 );
+	} );
+
+	it( 'should be overridden by set()', function() {
+		config.define( 'test', 1 );
+		config.set( 'test', 2 );
+
+		expect( config ).to.have.property( 'test' ).to.equals( 2 );
+		expect( config.get( 'test' ) ).to.equals( 2 );
+	} );
+} );

+ 9 - 1
packages/ckeditor5-engine/tests/editor/editor.js

@@ -7,7 +7,7 @@
 
 'use strict';
 
-var modules = bender.amd.require( 'editor' );
+var modules = bender.amd.require( 'editor', 'editorconfig' );
 
 var editor;
 var element;
@@ -27,6 +27,14 @@ describe( 'constructor', function() {
 	} );
 } );
 
+describe( 'config', function() {
+	it( 'should be an instance of EditorConfig', function() {
+		var EditorConfig = modules.editorconfig;
+
+		expect( editor.config ).to.be.an.instanceof( EditorConfig );
+	} );
+} );
+
 describe( 'destroy', function() {
 	it( 'should fire "destroy"', function() {
 		var spy = sinon.spy();

+ 46 - 0
packages/ckeditor5-engine/tests/editorconfig/editorconfig.js

@@ -0,0 +1,46 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals describe, it, expect, beforeEach */
+
+'use strict';
+
+var modules = bender.amd.require( 'editorconfig', 'ckeditor' );
+
+var config;
+
+beforeEach( function() {
+	var EditorConfig = modules.editorconfig;
+
+	config = new EditorConfig( {
+		test: 1
+	} );
+} );
+
+describe( 'constructor', function() {
+	it( 'should set configurations', function() {
+		expect( config ).to.have.property( 'test' ).to.equals( 1 );
+	} );
+} );
+
+describe( 'get', function() {
+	it( 'should retrieve a configuration', function() {
+		expect( config.get( 'test' ) ).to.equals( 1 );
+	} );
+
+	it( 'should fallback to CKEDITOR.config', function() {
+		var CKEDITOR = modules.ckeditor;
+
+		CKEDITOR.config.set( {
+			globalConfig: 2
+		} );
+
+		expect( config.get( 'globalConfig' ) ).to.equals( 2 );
+	} );
+
+	it( 'should return undefined for non existing configuration', function() {
+		expect( config.get( 'invalid' ) ).to.be.undefined();
+	} );
+} );