Bladeren bron

Initial work on porting tests to ES6 modules.

Piotrek Koszuliński 10 jaren geleden
bovenliggende
commit
75b0db0821

+ 4 - 4
bender.js

@@ -28,8 +28,7 @@ const config = {
 		all: {
 			applications: [ 'ckeditor' ],
 			paths: [
-				'tests/**',
-				'node_modules/ckeditor5-*/tests/**',
+				'dist/amd/tests/**',
 				'!**/_*/**'
 			]
 		}
@@ -37,8 +36,9 @@ const config = {
 
 	coverage: {
 		paths: [
-			'dist/amd/**/*.js',
-			'!dist/amd/ckeditor5-*/lib/**'
+			'dist/amd/ckeditor.js',
+			'dist/amd/ckeditor5/**/*.js',
+			'!dist/amd/ckeditor5/*/lib/**'
 		]
 	}
 };

+ 1 - 1
ckeditor.js

@@ -5,6 +5,6 @@
 
 'use strict';
 
-import CKEDITOR from './ckeditor5-core/ckeditor.js';
+import CKEDITOR from './ckeditor5/core/ckeditor.js';
 
 export default CKEDITOR;

+ 5 - 8
dev/bender/plugins/ckeditor5/lib/index.js

@@ -7,8 +7,7 @@
 
 const path = require( 'path' );
 const files = [
-	path.join( __dirname, '../static/amd.js' ),
-	path.join( __dirname, '../static/tools.js' )
+	path.join( __dirname, '../static/extensions.js' )
 ];
 
 module.exports = {
@@ -18,8 +17,8 @@ module.exports = {
 		this.plugins.addFiles( files );
 
 		this.on( 'test:created', ( test ) => {
-			const moduleRegExp = /node_modules\/ckeditor5-([^\/]+)/;
-			let name = test.displayName;
+			const moduleRegExp = /^([^\/]+)\//;
+			let name = test.displayName.replace( /^dist\/amd\/tests\//, '' );
 			let module = name.match( moduleRegExp );
 
 			if ( module ) {
@@ -27,13 +26,11 @@ module.exports = {
 				test.displayName = name.replace( moduleRegExp, '$1: ' );
 			} else {
 				test.tags.unshift( 'module!ckeditor5' );
-				test.displayName = 'ckeditor5: ' + test.displayName;
+				test.displayName = 'ckeditor5: ' + name.replace( /^ckeditor5\//, '' );
 			}
 		} );
 
-		// Add this plugins' scripts before the includes pagebuilder (which handles bender-include directives), so
-		// the main tools file is loaded before tools included in the core or in the plugins.
-		this.pagebuilders.add( 'ckeditor5', build, this.pagebuilders.getPriority( 'includes' ) - 1 );
+		this.pagebuilders.add( 'ckeditor5', build );
 
 		function build( data ) {
 			files.forEach( ( file ) => {

+ 0 - 157
dev/bender/plugins/ckeditor5/static/amd.js

@@ -1,157 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* jshint node: false, browser: true, globalstrict: true */
-/* globals bender, require, define */
-
-'use strict';
-
-( () => {
-	/**
-	 * AMD tools related to CKEditor.
-	 */
-	bender.amd = {
-		/**
-		 * Generates an absolute path to an AMD version of a CKEditor module. The function takes care of
-		 * generating a base path for that file, taking into account whether a Bender job is being run
-		 * or a simple test.
-		 *
-		 * The name should be given in a simplified features naming convention. See {@link ckeditor5/path#getModulePath}
-		 * for more details.
-		 *
-		 * @param {String} name The name of the module.
-		 * @returns {String} The absolute path to the module.
-		 */
-		getModulePath( name ) {
-			let appBasePath = bender.basePath;
-			let ckeditorBasePath = bender.config.applications.ckeditor.basePath;
-			let moduleBasePath;
-
-			// Reported: https://github.com/benderjs/benderjs/issues/248
-			// Ugh... make some paths cleanup, because we need to combine these fragments and we don't want to
-			// duplicate '/'. BTW. if you want to touch this make sure you haven't broken Bender jobs which
-			// have different bender.basePaths (no trailing '/', unnecessary 'tests/' fragment).
-			moduleBasePath =
-				appBasePath
-					.split( '/' )
-					.filter( nonEmpty )
-					// When running a job we need to drop the last parth of the base path, which is "tests".
-					.slice( 0, -1 )
-					.concat(
-						ckeditorBasePath.split( '/' ).filter( nonEmpty )
-					)
-					.join( '/' );
-
-			// NOTE: This code is duplicated in CKEDITOR.getModulePath() because we're not able to use here
-			// that function. It may be possible to resolve this once we start using ES6 modules and transpilation
-			// also for tests.
-			if ( name != 'ckeditor' ) {
-				// Resolve shortened feature names to `featureName/featureName`.
-				if ( name.indexOf( '/' ) < 0 ) {
-					name = name + '/' + name;
-				}
-
-				// Add the prefix to shortened paths like `core/editor` (will be `ckeditor5-core/editor`).
-				// Don't add the prefix to the main file and files frok ckeditor5/ module.
-				if ( !( /^ckeditor5\//.test( name ) ) ) {
-					name = 'ckeditor5-' + name;
-				}
-			}
-
-			return '/' + moduleBasePath + '/' + name + '.js';
-		},
-
-		/**
-		 * Shorthand for defining an AMD module.
-		 *
-		 * Note that the module and dependency names must be passed in the simplified features naming convention
-		 * (see {@link #getModulePath}).
-		 *
-		 * For simplicity the dependencies passed to the `body` will be unwrapped
-		 * from the ES6 module object (so only the default export will be available). Also the returned value
-		 * will be automatically handled as a default export.
-		 *
-		 * If you need to define a module which has access to other exports or can export more values,
-		 * use the global `define()` function:
-		 *
-		 *		define( bender.amd.getModulePath( name ), [ 'exports', 'foo', ... ].map( bender.amd.getModulePath ), ( FooModule, ... ) {
-		 *			const FooClass = FooModule.default;
-		 *			const FooOtherProp = FooModule.otherProp;
-		 *
-		 *			exports.default = MyClass;
-		 *			exports.otherProp = 1;
-		 *		} );
-		 *
-		 * **Note:** Since this method automatically unwraps modules from the ES6 module object when passing them
-		 * to the `body` function, circular dependencies will not work. If you need them, either use the raw `define()`
-		 * as shown above, or keep all the definitions outside modules and only access the variables from the scope:
-		 *
-		 *		PluginE = createPlugin( 'E' );
-		 *		PluginF = createPlugin( 'F' );
-		 *
-		 *		PluginF.requires = [ PluginE ];
-		 *		PluginE.requires = [ PluginF ];
-		 *
-		 *		bender.amd.define( 'E', [ 'core/plugin', 'F' ], () => {
-		 *			return PluginE;
-		 *		} );
-		 *
-		 *		bender.amd.define( 'F', [ 'core/plugin', 'E' ], () => {
-		 *			return PluginF;
-		 *		} );
-		 *
-		 * @param {String} name Name of the module.
-		 * @param {String[]} deps Dependencies of the module.
-		 * @param {Function} body Module body.
-		 */
-		define( name, deps, body ) {
-			if ( arguments.length == 2 ) {
-				body = deps;
-				deps = [];
-			}
-
-			const depsPaths = deps.map( bender.amd.getModulePath );
-
-			// Use the exports object instead of returning from modules in order to handle circular deps.
-			// http://requirejs.org/docs/api.html#circular
-			depsPaths.unshift( 'exports' );
-
-			define( bender.amd.getModulePath( name ), depsPaths, function( exports ) {
-				const loadedDeps = Array.from( arguments ).slice( 1 ).map( ( module ) => module.default );
-
-				exports.default = body.apply( this, loadedDeps );
-			} );
-		},
-
-		/**
-		 * Gets an object which holds the CKEditor modules guaranteed to be loaded before tests start.
-		 *
-		 * @params {...String} module The name of the module to load.
-		 * @returns {Object} The object that will hold the loaded modules.
-		 */
-		require() {
-			const modules = {};
-			const done = bender.defer();
-
-			const names = Array.from( arguments );
-			const modulePaths = names.map( bender.amd.getModulePath );
-
-			require( modulePaths, function() {
-				for ( let i = 0; i < names.length; i++ ) {
-					modules[ names[ i ] ] = arguments[ i ].default;
-				}
-
-				// Finally give green light for tests to start.
-				done();
-			} );
-
-			return modules;
-		}
-	};
-
-	function nonEmpty( str ) {
-		return !!str.length;
-	}
-} )();

+ 49 - 0
dev/bender/plugins/ckeditor5/static/extensions.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint node: false, browser: true, globalstrict: true, varstmt: false */
+/* globals bender, requirejs */
+
+'use strict';
+
+( () => {
+	// This seems to be the only way to force Require.JS to load modules starting with '/' from a different path.
+	var load = requirejs.load;
+	requirejs.load = function( context, moduleId, url ) {
+		var basePath = bender.getCKEditorModuleBasePath().replace( /\/$/, '' );
+
+		if ( moduleId[ 0 ] == '/' ) {
+			url = basePath + url;
+		}
+
+		return load( context, moduleId, url );
+	};
+
+	// Reported: https://github.com/benderjs/benderjs/issues/248
+	// Ugh... make some paths cleanup, because we need to combine these fragments and we don't want to
+	// duplicate '/'. BTW. if you want to touch this make sure you haven't broken Bender jobs which
+	// have different bender.basePaths (no trailing '/', unnecessary 'tests/' fragment).
+	bender.getCKEditorModuleBasePath = function() {
+		var appBasePath = bender.basePath;
+		var ckeditorBasePath = bender.config.applications.ckeditor.basePath;
+		var modulebasePath;
+
+		modulebasePath = appBasePath
+			.split( '/' )
+			.filter( nonEmpty )
+			// When running a job we need to drop the last parth of the base path, which is "tests".
+			.slice( 0, -1 )
+			.concat(
+				ckeditorBasePath.split( '/' ).filter( nonEmpty )
+			)
+			.join( '/' );
+
+		return '/' + modulebasePath;
+	};
+
+	function nonEmpty( str ) {
+		return !!str.length;
+	}
+} )();

+ 0 - 52
dev/bender/plugins/ckeditor5/static/tools.js

@@ -1,52 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* jshint node: false, browser: true, globalstrict: true */
-/* globals bender, before, afterEach, sinon */
-
-'use strict';
-
-( () => {
-	/**
-	 * Test tools for CKEditor.
-	 *
-	 * This is a main namespace for the test tools.
-	 *
-	 * * General tools go directly to `bender.tools.*`.
-	 * * Core tools (introduced by `ckeditor5-core`) go to `bender.tools.core.*`.
-	 * * Plugin tools (introduced by plugins) go to `bender.tools.plugin.<plugin-name>.*`.
-	 *
-	 * Tools for specific plugins or the core should be kept in `tests/_tools/tools.js` file
-	 * of the respective repository. They can be loaded using Bender's `bender-include` directive.
-	 * Their tests should be kept in `tests/bender/*` directory.
-	 */
-	bender.tools = {
-		/**
-		 * Creates Sinon sandbox in {@link bender#sinon} and plugs `afterEach()` callback which
-		 * restores all spies and stubs created in this sandbox.
-		 *
-		 * See https://github.com/ckeditor/ckeditor5-design/issues/72 and http://sinonjs.org/docs/#sinon-sandbox
-		 *
-		 * Usage:
-		 *
-		 *		// Directly in the test file:
-		 *		bender.tools.createSinonSandbox();
-		 *
-		 *		// Then inside tests you can use bender.sinon:
-		 *		it( 'does something', () => {
-		 *			bender.sinon.spy( obj, 'method' );
-		 *		} );
-		 */
-		createSinonSandbox() {
-			before( () => {
-				bender.sinon = sinon.sandbox.create();
-			} );
-
-			afterEach( () => {
-				bender.sinon.restore();
-			} );
-		}
-	};
-} )();

+ 2 - 2
dev/tasks/gulp/build/tasks.js

@@ -74,7 +74,7 @@ module.exports = ( config ) => {
 			 * @returns {Stream}
 			 */
 			ckeditor5( watch ) {
-				const glob = path.join( config.ROOT_DIR, 'src', '**', '*.js' );
+				const glob = path.join( config.ROOT_DIR, '@(src|tests)', '**', '*.js' );
 
 				return gulp.src( glob )
 					.pipe( watch ? gulpWatch( glob ) : utils.noop() )
@@ -114,7 +114,7 @@ module.exports = ( config ) => {
 					.filter( ( filePath ) => filePath );
 
 				const streams = dirs.map( ( dirPath ) => {
-					const glob = path.join( dirPath, 'src', '**', '*.js' );
+					const glob = path.join( dirPath, '@(src|tests)', '**', '*.js' );
 					// Use parent as a base so we get paths starting with 'ckeditor5-*/src/*' in the stream.
 					const baseDir = path.parse( dirPath ).dir;
 					const opts = { base: baseDir };

+ 148 - 52
dev/tasks/gulp/build/utils.js

@@ -5,13 +5,30 @@
 const path = require( 'path' );
 const gulp = require( 'gulp' );
 const rename = require( 'gulp-rename' );
-const babel = require( 'gulp-babel' );
+const gulpBabel = require( 'gulp-babel' );
 const gutil = require( 'gulp-util' );
+const gulpFilter = require( 'gulp-filter' );
 const multipipe = require( 'multipipe' );
 const PassThrough = require( 'stream' ).PassThrough;
+const through = require( 'through2' );
 
 const utils = {
 	/**
+	 * Code which can be appended to transpiled (into AMD) test files in order to
+	 * load the 'tests' module and defer launching Bender until it's ready.
+	 *
+	 * Note: This code will not be transpiled so keep it in ES5.
+	 */
+	benderLauncherCode:
+`
+require( [ 'tests' ], bender.defer(), function( err ) {
+	// The problem with Require.JS is that there are no stacktraces if we won't log this.
+	console.error( err );
+	console.log( err.stack );
+} );
+`,
+
+	/**
 	 * Creates a pass-through stream.
 	 *
 	 * @returns {Stream}
@@ -34,47 +51,6 @@ const utils = {
 	},
 
 	/**
-	 * Transpiles files piped into this stream to the given format (`amd` or `cjs`).
-	 *
-	 * @param {String} format
-	 * @returns {Stream}
-	 */
-	transpile( format ) {
-		const babelModuleTranspilers = {
-			amd: 'amd',
-			cjs: 'commonjs'
-		};
-		const babelModuleTranspiler = babelModuleTranspilers[ format ];
-
-		if ( !babelModuleTranspiler ) {
-			throw new Error( `Incorrect format: ${ format }` );
-		}
-
-		return babel( {
-				plugins: [
-					// Note: When plugin is specified by its name, Babel loads it from a context of a
-					// currently transpiled file (in our case - e.g. from ckeditor5-core/src/foo.js).
-					// Obviously that fails, since we have all the plugins installed only in ckeditor5/
-					// and we want to have them only there to avoid installing them dozens of times.
-					//
-					// Anyway, I haven't found in the docs that you can also pass a plugin instance here,
-					// but it works... so let's hope it will.
-					require( `babel-plugin-transform-es2015-modules-${ babelModuleTranspiler }` )
-				],
-				// Ensure that all paths ends with '.js' because Require.JS (unlike Common.JS/System.JS)
-				// will not add it to module names which look like paths.
-				resolveModuleSource: ( source ) => {
-					return utils.appendModuleExtension( source );
-				}
-			} )
-			.on( 'error', function( err ) {
-				gutil.log( gutil.colors.red( `Error (Babel:${ format })` ) );
-				gutil.log( gutil.colors.red( err.message ) );
-				console.log( '\n' + err.codeFrame + '\n' );
-			} );
-	},
-
-	/**
 	 * Creates a function generating convertion streams.
 	 * Used to generate `formats.reduce()` callback where `formats` is an array of formats that should be generated.
 	 *
@@ -88,7 +64,22 @@ const utils = {
 			conversionPipes.push( utils.pickVersionedFile( format ) );
 
 			if ( format != 'esnext' ) {
-				conversionPipes.push( utils.transpile( format ) );
+				const filterCode = gulpFilter( utils.isNotTestFile, { restore: true } );
+				const transpileCode = utils.transpile( format, utils.getBabelOptionsForCode( format ) );
+				conversionPipes.push(
+					filterCode,
+					transpileCode,
+					filterCode.restore
+				);
+
+				const filterTests = gulpFilter( utils.isTestFile, { restore: true } );
+				const transpileTests = utils.transpile( format, utils.getBabelOptionsForTests( format ) );
+				conversionPipes.push(
+					filterTests,
+					transpileTests,
+					utils.appendBenderLauncher(),
+					filterTests.restore
+				);
 			}
 
 			conversionPipes.push(
@@ -105,6 +96,70 @@ const utils = {
 	},
 
 	/**
+	 * Transpiles files piped into this stream to the given format (`amd` or `cjs`).
+	 *
+	 * @param {String} format
+	 * @returns {Stream}
+	 */
+	transpile( format, options ) {
+		return gulpBabel( options )
+			.on( 'error', function( err ) {
+				gutil.log( gutil.colors.red( `Error (Babel:${ format })` ) );
+				gutil.log( gutil.colors.red( err.message ) );
+				console.log( '\n' + err.codeFrame + '\n' );
+			} );
+	},
+
+	getBabelOptionsForCode( format ) {
+		return {
+			plugins: utils.getBabelPlugins( format ),
+			// Ensure that all paths ends with '.js' because Require.JS (unlike Common.JS/System.JS)
+			// will not add it to module names which look like paths.
+			resolveModuleSource: utils.appendModuleExtension
+		};
+	},
+
+	getBabelOptionsForTests( format ) {
+		return {
+			plugins: utils.getBabelPlugins( format ),
+			resolveModuleSource: utils.appendModuleExtension,
+			moduleIds: true,
+			moduleId: 'tests'
+		};
+	},
+
+	getBabelPlugins( format ) {
+		const babelModuleTranspilers = {
+			amd: 'amd',
+			cjs: 'commonjs'
+		};
+		const babelModuleTranspiler = babelModuleTranspilers[ format ];
+
+		if ( !babelModuleTranspiler ) {
+			throw new Error( `Incorrect format: ${ format }` );
+		}
+
+		return [
+			// Note: When plugin is specified by its name, Babel loads it from a context of a
+			// currently transpiled file (in our case - e.g. from ckeditor5-core/src/foo.js).
+			// Obviously that fails, since we have all the plugins installed only in ckeditor5/
+			// and we want to have them only there to avoid installing them dozens of times.
+			//
+			// Anyway, I haven't found in the docs that you can also pass a plugin instance here,
+			// but it works... so let's hope it will.
+			require( `babel-plugin-transform-es2015-modules-${ babelModuleTranspiler }` )
+		];
+	},
+
+	appendBenderLauncher() {
+		return through( { objectMode: true }, ( file, encoding, callback ) => {
+			file.contents = new Buffer( file.contents.toString() + utils.benderLauncherCode );
+
+			callback( null, file );
+		} );
+	},
+
+	/**
 	 * Allows us to pick one of files suffixed with the format (`__esnext`, `__amd`, or `__cjs`).
 	 *
 	 * For example: we have `load__esnext.js`, `load__amd.js` and `load__cjs.js`. After applying this
@@ -121,29 +176,44 @@ const utils = {
 		} );
 	},
 
+	// TODO
+	// Update the names of the next two methods and their docs.
+
 	/**
 	 * Moves files out of `ckeditor5-xxx/src/*` directories to `ckeditor5-xxx/*`.
+	 * And `ckeditor5-xxx/tests/*` to `tests/ckeditor5-xxx/`.
 	 *
 	 * @returns {Stream}
 	 */
 	unpackPackages() {
 		return rename( ( file ) => {
-			const dir = file.dirname.split( path.sep );
+			const dirFrags = file.dirname.split( path.sep );
 
 			// Validate the input for the clear conscious.
 
-			if ( dir[ 0 ].indexOf( 'ckeditor5-' ) !== 0 ) {
+			if ( dirFrags[ 0 ].indexOf( 'ckeditor5-' ) !== 0 ) {
 				throw new Error( 'Path should start with "ckeditor5-".' );
 			}
 
-			if ( dir[ 1 ] != 'src' ) {
-				throw new Error( 'Path should start with "ckeditor5-*/src".' );
-			}
+			dirFrags[ 0 ] = dirFrags[ 0 ].replace( /^ckeditor5-/, '' );
+
+			const firstFrag = dirFrags[ 1 ];
 
-			// Remove 'src'.
-			dir.splice( 1, 1 );
+			if ( firstFrag == 'src' ) {
+				// Remove 'src/'.
+				dirFrags.splice( 1, 1 );
 
-			file.dirname = path.join.apply( null, dir );
+				dirFrags.unshift( 'ckeditor5' );
+			} else if ( firstFrag == 'tests' ) {
+				// Remove 'tests/' from the package dir.
+				dirFrags.splice( 1, 1 );
+				// And prepend 'tests/'.
+				dirFrags.unshift( 'tests' );
+			} else {
+				throw new Error( 'Path should start with "ckeditor5-*/(src|tests)".' );
+			}
+
+			file.dirname = path.join.apply( null, dirFrags );
 		} );
 	},
 
@@ -154,7 +224,18 @@ const utils = {
 	 */
 	wrapCKEditor5Package() {
 		return rename( ( file ) => {
-			file.dirname = path.join( 'ckeditor5', file.dirname );
+			const dirFrags = file.dirname.split( path.sep );
+			const firstFrag = dirFrags[ 0 ];
+
+			if ( firstFrag == 'src' ) {
+				// Replace 'src/' with 'ckeditor5/'.
+				// src/path.js -> ckeditor5/path.js
+				dirFrags.splice( 0, 1, 'ckeditor5' );
+			} else if ( firstFrag != 'tests' ) {
+				throw new Error( 'Path should start with "src" or "tests".' );
+			}
+
+			file.dirname = path.join.apply( null, dirFrags );
 		} );
 	},
 
@@ -170,6 +251,21 @@ const utils = {
 		}
 
 		return source;
+	},
+
+	isTestFile( file ) {
+		// TODO this should be based on bender configuration (config.tests.*.paths).
+		if ( !file.relative.startsWith( 'tests/' ) ) {
+			return false;
+		}
+
+		const dirFrags = file.relative.split( path.sep );
+
+		return !dirFrags.some( dirFrag => dirFrag.startsWith( '_' ) );
+	},
+
+	isNotTestFile( file ) {
+		return !utils.isTestFile( file );
 	}
 };
 

+ 3 - 1
package.json

@@ -31,6 +31,7 @@
     "grunt-text-replace": "^0.4.0",
     "gulp": "^3.9.0",
     "gulp-babel": "^6.1.0",
+    "gulp-filter": "^3.0.1",
     "gulp-mirror": "^0.4.0",
     "gulp-rename": "^1.2.2",
     "gulp-sourcemaps": "^1.6.0",
@@ -46,7 +47,8 @@
     "ncp": "^2.0.0",
     "replace": "^0.3.0",
     "shelljs": "^0",
-    "sinon": "^1.17.0"
+    "sinon": "^1.17.0",
+    "through2": "^2.0.0"
   },
   "author": "CKSource (http://cksource.com/)",
   "license": "See LICENSE.md",

+ 1 - 1
src/load__amd.js

@@ -12,7 +12,7 @@ import require from 'require';
 /**
  * Loads a module.
  *
- *		load( CKEDITOR.getModulePath( 'core/editor' ) )
+ *		load( 'ckeditor5/core/editor.js' )
  *			.then( ( EditorModule ) => {
  *				const Editor = EditorModule.default;
  *			} );

+ 2 - 35
src/path.js

@@ -7,6 +7,8 @@
 
 const path = {
 	/**
+	 * TODO: review whether this property is needed https://github.com/ckeditor/ckeditor5/issues/61.
+	 *
 	 * The full URL for the CKEditor installation directory.
 	 *
 	 * It is possible to manually provide the base path by setting a global variable named `CKEDITOR_BASEPATH`. This
@@ -20,41 +22,6 @@ const path = {
 	basePath: getBasePath(),
 
 	/**
-	 * Resolves a simplified module name convention to a real path. The returned
-	 * paths are relative to the main `ckeditor.js` file, but they do not start with `./`.
-	 *
-	 * For instance:
-	 *
-	 * * `foo` will be transformed to `ckeditor5-foo/foo.js`,
-	 * * `ckeditor` to `ckeditor.js`,
-	 * * `core/editor` to `ckeditor5-core/editor.js` and
-	 * * `foo/bar/bom` to `ckeditor5-foo/bar/bom.js`.
-	 *
-	 * @param {String} name
-	 * @returns {String} Path to the module.
-	 */
-	getModulePath( name ) {
-		//
-		// Note: This piece of code is duplicated in bender.amd.getModulePath().
-		//
-
-		if ( name != 'ckeditor' ) {
-			// Resolve shortened feature names to `featureName/featureName`.
-			if ( name.indexOf( '/' ) < 0 ) {
-				name = name + '/' + name;
-			}
-
-			// Add the prefix to shortened paths like `core/editor` (will be `ckeditor5-core/editor`).
-			// Don't add the prefix to the main file and files frok ckeditor5/ module.
-			if ( !( /^ckeditor5\//.test( name ) ) ) {
-				name = 'ckeditor5-' + name;
-			}
-		}
-
-		return name + '.js';
-	},
-
-	/**
 	 * Computes the value of the `basePath` property.
 	 *
 	 * @private

+ 111 - 0
tests/_utils/amd.js

@@ -0,0 +1,111 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals bender, define, require */
+
+'use strict';
+
+/**
+ * AMD tools related to CKEditor.
+ */
+const amdUtils = {
+	getModulePath( modulePath ) {
+		if ( modulePath.indexOf( '/' ) < 0 ) {
+			modulePath = modulePath + '/' + modulePath;
+		}
+
+		return '/ckeditor5/' + modulePath + '.js';
+	},
+
+	/**
+	 * Shorthand for defining an AMD module.
+	 *
+	 * For simplicity the dependencies passed to the `body` will be unwrapped
+	 * from the ES6 module object (so only the default export will be available). Also the returned value
+	 * will be automatically handled as a default export.
+	 *
+	 * If you need to define a module which has access to other exports or can export more values,
+	 * use the global `define()` function:
+	 *
+	 *		define( '/ckeditor5/my/module.js', [ 'exports', '/ckeditor5/foo/foo.js', ... ], ( FooModule, ... ) {
+	 *			const FooClass = FooModule.default;
+	 *			const FooOtherProp = FooModule.otherProp;
+	 *
+	 *			exports.default = MyClass;
+	 *			exports.otherProp = 1;
+	 *		} );
+	 *
+	 * **Note:** Since this method automatically unwraps modules from the ES6 module object when passing them
+	 * to the `body` function, circular dependencies will not work. If you need them, either use the raw `define()`
+	 * as shown above, or keep all the definitions outside modules and only access the variables from the scope:
+	 *
+	 *		PluginE = createPlugin( 'E' );
+	 *		PluginF = createPlugin( 'F' );
+	 *
+	 *		PluginF.requires = [ PluginE ];
+	 *		PluginE.requires = [ PluginF ];
+	 *
+	 *		amdUtils.define( '/ckeditor5/E/E.js', [ '/ckeditor5/core/plugin.js', '/ckeditor5/F/F.js' ], () => {
+	 *			return PluginE;
+	 *		} );
+	 *
+	 *		amdUtils.define( '/ckeditor5/F/F.js', [ '/ckeditor5/core/plugin.js', '/ckeditor5/E/E.js' ], () => {
+	 *			return PluginF;
+	 *		} );
+	 *
+	 * @param {String} path Parh to the module.
+	 * @param {String[]} deps Dependencies of the module.
+	 * @param {Function} body Module body.
+	 */
+	define( path, deps, body ) {
+		if ( arguments.length == 2 ) {
+			body = deps;
+			deps = [];
+		}
+
+		deps = deps.map( amdUtils.getModulePath );
+
+		// Use the exports object instead of returning from modules in order to handle circular deps.
+		// http://requirejs.org/docs/api.html#circular
+		deps.unshift( 'exports' );
+
+		define( amdUtils.getModulePath( path ), deps, function( exports ) {
+			const loadedDeps = Array.from( arguments ).slice( 1 ).map( ( module ) => module.default );
+
+			exports.default = body.apply( this, loadedDeps );
+		} );
+	},
+
+	/**
+	 * Gets an object which holds the CKEditor modules guaranteed to be loaded before tests start.
+	 *
+	 * @params {...String} module The name of the module to load.
+	 * @returns {Object} The object that will hold the loaded modules.
+	 */
+	require( modules ) {
+		const resolved = {};
+		const paths = [];
+		const names = [];
+		const done = bender.defer();
+
+		for ( let name in modules ) {
+			names.push( name );
+			paths.push( amdUtils.getModulePath( modules[ name ] ) );
+		}
+
+		require( paths, function( ...loaded ) {
+			for ( let i = 0; i < names.length; i++ ) {
+				resolved[ names[ i ] ] = loaded[ i ].default;
+			}
+
+			// Finally give green light for tests to start.
+			done();
+		} );
+
+		return resolved;
+	}
+};
+
+export default amdUtils;

+ 42 - 0
tests/_utils/utils.js

@@ -0,0 +1,42 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint node: false, browser: true, globalstrict: true */
+/* globals before, afterEach, sinon */
+
+'use strict';
+
+/**
+ * General test utils for CKEditor.
+ */
+const utils = {
+	/**
+	 * Creates Sinon sandbox in {@link bender#sinon} and plugs `afterEach()` callback which
+	 * restores all spies and stubs created in this sandbox.
+	 *
+	 * See https://github.com/ckeditor/ckeditor5-design/issues/72 and http://sinonjs.org/docs/#sinon-sandbox
+	 *
+	 * Usage:
+	 *
+	 *		// Directly in the test file:
+	 *		testUtils.createSinonSandbox();
+	 *
+	 *		// Then inside tests you can use bender.sinon:
+	 *		it( 'does something', () => {
+	 *			testUtils.sinon.spy( obj, 'method' );
+	 *		} );
+	 */
+	createSinonSandbox() {
+		before( () => {
+			utils.sinon = sinon.sandbox.create();
+		} );
+
+		afterEach( () => {
+			utils.sinon.restore();
+		} );
+	}
+};
+
+export default utils;

+ 0 - 135
tests/bender/amd.js

@@ -1,135 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* global require */
-
-'use strict';
-
-bender.tools.createSinonSandbox();
-
-describe( 'bender.amd', () => {
-	const getModulePath = bender.amd.getModulePath;
-
-	describe( 'getModulePath()', () => {
-		// Thanks to this we'll check whether all paths are relative to ckeditor.js path.
-		const basePath = getModulePath( 'ckeditor' ).replace( /\/ckeditor\.js$/, '/' );
-
-		it( 'generates path for the main file', () => {
-			const path = getModulePath( 'ckeditor' );
-
-			expect( path ).to.match( /\/ckeditor.js$/, 'ends with /ckeditor.js' );
-			expect( path ).to.match( /^\//, 'is absolute' );
-		} );
-
-		it( 'generates path for modules within ckeditor5 package', () => {
-			const path = getModulePath( 'ckeditor5/foo' );
-
-			expect( path ).to.equal( basePath + 'ckeditor5/foo.js' );
-		} );
-
-		it( 'generates path for modules within the core package', () => {
-			const path = getModulePath( 'core/ui/controller' );
-
-			expect( path ).to.equal( basePath + 'ckeditor5-core/ui/controller.js' );
-		} );
-
-		it( 'generates path for modules within some package', () => {
-			const path = getModulePath( 'some/ba' );
-
-			expect( path ).to.equal( basePath + 'ckeditor5-some/ba.js' );
-		} );
-
-		it( 'generates path from simplified feature name', () => {
-			const path = getModulePath( 'foo' );
-
-			expect( path ).to.equal( basePath + 'ckeditor5-foo/foo.js' );
-		} );
-	} );
-
-	describe( 'define()', () => {
-		it( 'defines a module by using global define()', () => {
-			const spy = bender.sinon.spy( window, 'define' );
-			const expectedDeps = [ 'exports' ].concat( [ 'bar', 'ckeditor' ].map( getModulePath ) );
-
-			bender.amd.define( 'test1', [ 'bar', 'ckeditor' ], () => {} );
-
-			expect( spy.calledOnce ).to.be.true;
-			expect( spy.args[ 0 ][ 0 ] ).to.equal( getModulePath( 'test1' ) );
-			expect( spy.args[ 0 ][ 1 ] ).to.deep.equal( expectedDeps );
-		} );
-
-		it( 'maps body args and returned value', () => {
-			const spy = bender.sinon.spy( window, 'define' );
-			const bodySpy = sinon.spy( () => 'ret' );
-
-			bender.amd.define( 'test2', [ 'bar', 'ckeditor' ], bodySpy );
-
-			const realBody = spy.args[ 0 ][ 2 ];
-			const exportsObj = {};
-
-			expect( realBody ).to.be.a( 'function' );
-
-			realBody( exportsObj, { default: 'arg' } );
-
-			expect( exportsObj ).to.have.property( 'default', 'ret', 'it wraps the ret value with an ES6 module obj' );
-			expect( bodySpy.calledOnce ).to.be.true;
-			expect( bodySpy.args[ 0 ][ 0 ] ).to.equal( 'arg', 'it unwraps the args' );
-		} );
-
-		it( 'works with module name and body', () => {
-			const spy = bender.sinon.spy( window, 'define' );
-
-			bender.amd.define( 'test1', () => {} );
-
-			expect( spy.calledOnce ).to.be.true;
-			expect( spy.args[ 0 ][ 0 ] ).to.equal( getModulePath( 'test1' ) );
-			expect( spy.args[ 0 ][ 1 ] ).to.deep.equal( [ 'exports' ] );
-			expect( spy.args[ 0 ][ 2 ] ).to.be.a( 'function' );
-		} );
-
-		// Note: this test only checks whether Require.JS doesn't totally fail when creating a circular dependency.
-		// The value of dependencies are not available anyway inside the bender.amd.define() callbacks because
-		// we lose the late-binding by immediately mapping modules to their default exports.
-		it( 'works with circular dependencies', ( done ) => {
-			bender.amd.define( 'test-circular-a', [ 'test-circular-b' ], () => {
-				return 'a';
-			} );
-
-			bender.amd.define( 'test-circular-b', [ 'test-circular-a' ], () => {
-				return 'b';
-			} );
-
-			require( [ 'test-circular-a', 'test-circular-b' ].map( bender.amd.getModulePath ), ( a, b ) => {
-				expect( a ).to.have.property( 'default', 'a' );
-				expect( b ).to.have.property( 'default', 'b' );
-
-				done();
-			} );
-		} );
-	} );
-
-	describe( 'require', () => {
-		it( 'blocks Bender and loads modules through global require()', () => {
-			let requireCb;
-			const deferCbSpy = sinon.spy();
-
-			bender.sinon.stub( bender, 'defer', () => deferCbSpy );
-			bender.sinon.stub( window, 'require', ( deps, cb ) => {
-				requireCb = cb;
-			} );
-
-			const modules = bender.amd.require( 'foo', 'bar' );
-
-			expect( deferCbSpy.called ).to.be.false;
-
-			requireCb( { default: 1 }, { default: 2 } );
-
-			expect( deferCbSpy.calledOnce ).to.be.true;
-
-			expect( modules ).to.have.property( 'foo', 1 );
-			expect( modules ).to.have.property( 'bar', 2 );
-		} );
-	} );
-} );

+ 1 - 6
tests/ckeditor.js

@@ -5,12 +5,7 @@
 
 'use strict';
 
-const modules = bender.amd.require( 'ckeditor' );
-let CKEDITOR;
-
-before( () => {
-	CKEDITOR = modules.ckeditor;
-} );
+import CKEDITOR from '/ckeditor.js';
 
 describe( 'CKEDITOR', () => {
 	it( 'is an object', () => {

+ 3 - 9
tests/load.js

@@ -5,13 +5,7 @@
 
 'use strict';
 
-const modules = bender.amd.require( 'ckeditor5/path', 'ckeditor5/load' );
-let load, path;
-
-before( () => {
-	path = modules[ 'ckeditor5/path' ];
-	load = modules[ 'ckeditor5/load' ];
-} );
+import load from '/ckeditor5/load.js';
 
 describe( 'load()', () => {
 	it( 'loads ckeditor.js', () => {
@@ -21,8 +15,8 @@ describe( 'load()', () => {
 			} );
 	} );
 
-	it( 'loads core/editor', () => {
-		return load( path.getModulePath( 'core/editor' ) )
+	it( 'loads ckeditor5/core/editor', () => {
+		return load( 'ckeditor5/core/editor.js' )
 			.then( ( EditorModule ) => {
 				expect( EditorModule.default ).to.be.a( 'function' );
 			} );

+ 1 - 6
tests/path/basepath.js

@@ -5,12 +5,7 @@
 
 'use strict';
 
-const modules = bender.amd.require( 'ckeditor5/path' );
-let path;
-
-before( () => {
-	path = modules[ 'ckeditor5/path' ];
-} );
+import path from '/ckeditor5/path.js';
 
 beforeEach( () => {
 	// Ensure that no CKEDITOR_BASEPATH global is available.

+ 0 - 45
tests/path/getmodulepath.js

@@ -1,45 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-const modules = bender.amd.require( 'ckeditor5/path' );
-let path;
-
-before( () => {
-	path = modules[ 'ckeditor5/path' ];
-} );
-
-describe( 'getModulePath()', () => {
-	it( 'generates path for the main file', () => {
-		const p = path.getModulePath( 'ckeditor' );
-
-		expect( p ).to.equal( 'ckeditor.js' );
-	} );
-
-	it( 'generates path for modules within ckeditor5 package', () => {
-		const p = path.getModulePath( 'ckeditor5/foo' );
-
-		expect( p ).to.equal( 'ckeditor5/foo.js' );
-	} );
-
-	it( 'generates path for modules within the core package', () => {
-		const p = path.getModulePath( 'core/ui/controller' );
-
-		expect( p ).to.equal( 'ckeditor5-core/ui/controller.js' );
-	} );
-
-	it( 'generates path for modules within some package', () => {
-		const p = path.getModulePath( 'some/ba' );
-
-		expect( p ).to.equal( 'ckeditor5-some/ba.js' );
-	} );
-
-	it( 'generates path from simplified feature name', () => {
-		const p = path.getModulePath( 'foo' );
-
-		expect( p ).to.equal( 'ckeditor5-foo/foo.js' );
-	} );
-} );

+ 88 - 0
tests/utils-amd.js

@@ -0,0 +1,88 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global require */
+
+'use strict';
+
+import testUtils from '/tests/_utils/utils.js';
+import amdTestUtils from '/tests/_utils/amd.js';
+
+testUtils.createSinonSandbox();
+
+describe( 'bender.amd', () => {
+	const getModulePath = amdTestUtils.getModulePath;
+
+	describe( 'getModulePath()', () => {
+		it( 'generates an absolute path', () => {
+			const path = getModulePath( 'ckeditor' );
+
+			expect( path ).to.match( /\/ckeditor.js$/, 'ends with /ckeditor.js' );
+			expect( path ).to.match( /^\//, 'is absolute' );
+		} );
+	} );
+
+	describe( 'define()', () => {
+		it( 'defines a module by using global define()', () => {
+			const spy = testUtils.sinon.spy( window, 'define' );
+			const expectedDeps = [ 'exports' ].concat( [ 'bar', 'ckeditor' ].map( getModulePath ) );
+
+			amdTestUtils.define( 'test1', [ 'bar', 'ckeditor' ], () => {} );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( getModulePath( 'test1' ) );
+			expect( spy.args[ 0 ][ 1 ] ).to.deep.equal( expectedDeps );
+		} );
+
+		it( 'maps body args and returned value', () => {
+			const spy = testUtils.sinon.spy( window, 'define' );
+			const bodySpy = sinon.spy( () => 'ret' );
+
+			amdTestUtils.define( 'test2', [ 'bar', 'ckeditor' ], bodySpy );
+
+			const realBody = spy.args[ 0 ][ 2 ];
+			const exportsObj = {};
+
+			expect( realBody ).to.be.a( 'function' );
+
+			realBody( exportsObj, { default: 'arg' } );
+
+			expect( exportsObj ).to.have.property( 'default', 'ret', 'it wraps the ret value with an ES6 module obj' );
+			expect( bodySpy.calledOnce ).to.be.true;
+			expect( bodySpy.args[ 0 ][ 0 ] ).to.equal( 'arg', 'it unwraps the args' );
+		} );
+
+		it( 'works with module name and body', () => {
+			const spy = testUtils.sinon.spy( window, 'define' );
+
+			amdTestUtils.define( 'test1', () => {} );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( getModulePath( 'test1' ) );
+			expect( spy.args[ 0 ][ 1 ] ).to.deep.equal( [ 'exports' ] );
+			expect( spy.args[ 0 ][ 2 ] ).to.be.a( 'function' );
+		} );
+
+		// Note: this test only checks whether Require.JS doesn't totally fail when creating a circular dependency.
+		// The value of dependencies are not available anyway inside the amdTestUtils.define() callbacks because
+		// we lose the late-binding by immediately mapping modules to their default exports.
+		it( 'works with circular dependencies', ( done ) => {
+			amdTestUtils.define( 'test-circular-a', [ 'test-circular-b' ], () => {
+				return 'a';
+			} );
+
+			amdTestUtils.define( 'test-circular-b', [ 'test-circular-a' ], () => {
+				return 'b';
+			} );
+
+			require( [ 'test-circular-a', 'test-circular-b' ].map( amdTestUtils.getModulePath ), ( a, b ) => {
+				expect( a ).to.have.property( 'default', 'a' );
+				expect( b ).to.have.property( 'default', 'b' );
+
+				done();
+			} );
+		} );
+	} );
+} );

+ 7 - 5
tests/bender/createsinonsandbox.js

@@ -5,23 +5,25 @@
 
 'use strict';
 
+import testUtils from '/tests/_utils/utils.js';
+
 const obj = {
 	method() {}
 };
 const origMethod = obj.method;
 let spy;
 
-bender.tools.createSinonSandbox();
+testUtils.createSinonSandbox();
 
-describe( 'bender.tools.createSinonSandbox()', () => {
+describe( 'testUtils.createSinonSandbox()', () => {
 	it( 'creates a sandbox', () => {
-		expect( bender.sinon ).to.be.an( 'object' );
-		expect( bender.sinon ).to.have.property( 'spy' );
+		expect( testUtils.sinon ).to.be.an( 'object' );
+		expect( testUtils.sinon ).to.have.property( 'spy' );
 	} );
 
 	// This test is needed for the following one.
 	it( 'really works', () => {
-		spy = bender.sinon.spy( obj, 'method' );
+		spy = testUtils.sinon.spy( obj, 'method' );
 
 		expect( obj ).to.have.property( 'method', spy );
 	} );