diff --git a/.gitignore b/.gitignore
index 2a5e7539095..4ccb8a6e2d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
node_modules/
build
+tmp
.DS_Store
*.tgz
my-app*
diff --git a/bin/react-scripts.js b/bin/react-scripts.js
old mode 100644
new mode 100755
index 355d49f16f0..26d842d798f
--- a/bin/react-scripts.js
+++ b/bin/react-scripts.js
@@ -7,6 +7,7 @@ switch (script) {
case 'build':
case 'start':
case 'eject':
+case 'test':
spawn(
'node',
[require.resolve('../scripts/' + script)].concat(args),
diff --git a/config/paths.js b/config/paths.js
index b0f94588adb..dd6a334a8c0 100644
--- a/config/paths.js
+++ b/config/paths.js
@@ -31,34 +31,40 @@ if (isInCreateReactAppSource) {
// create-react-app development: we're in ./config/
module.exports = {
appBuild: resolve('../build'),
+ appTmp: resolve('../tmp'),
appHtml: resolve('../template/index.html'),
appFavicon: resolve('../template/favicon.ico'),
appPackageJson: resolve('../package.json'),
appSrc: resolve('../template/src'),
appNodeModules: resolve('../node_modules'),
- ownNodeModules: resolve('../node_modules')
+ ownNodeModules: resolve('../node_modules'),
+ testEntry: resolve('tests.webpack.debug.js')
};
} else if (isInNodeModules) {
// before eject: we're in ./node_modules/react-scripts/config/
module.exports = {
appBuild: resolve('../../../build'),
+ appTmp: resolve('../../../tmp'),
appHtml: resolve('../../../index.html'),
appFavicon: resolve('../../../favicon.ico'),
appPackageJson: resolve('../../../package.json'),
appSrc: resolve('../../../src'),
appNodeModules: resolve('../..'),
// this is empty with npm3 but node resolution searches higher anyway:
- ownNodeModules: resolve('../node_modules')
+ ownNodeModules: resolve('../node_modules'),
+ testEntry: resolve('tests.webpack.preeject.js')
};
} else {
// after eject: we're in ./config/
module.exports = {
appBuild: resolve('../build'),
+ appTmp: resolve('../tmp'),
appHtml: resolve('../index.html'),
appFavicon: resolve('../favicon.ico'),
appPackageJson: resolve('../package.json'),
appSrc: resolve('../src'),
appNodeModules: resolve('../node_modules'),
- ownNodeModules: resolve('../node_modules')
+ ownNodeModules: resolve('../node_modules'),
+ testEntry: resolve('tests.webpack.js')
};
}
diff --git a/config/tests.webpack.debug.js b/config/tests.webpack.debug.js
new file mode 100644
index 00000000000..81735f4c439
--- /dev/null
+++ b/config/tests.webpack.debug.js
@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+const context = require.context('../template/src', true, /\.js$/);
+context.keys()
+ .filter(path => path.match(/__tests__|\/test\/|\.(spec|test)\.js$/))
+ .forEach(context);
diff --git a/config/tests.webpack.js b/config/tests.webpack.js
new file mode 100644
index 00000000000..97882b553a2
--- /dev/null
+++ b/config/tests.webpack.js
@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+const context = require.context('../src', true, /\.js$/);
+context.keys()
+ .filter(path => path.match(/__tests__|\/test\/|\.(spec|test)\.js$/))
+ .forEach(context);
diff --git a/config/tests.webpack.preeject.js b/config/tests.webpack.preeject.js
new file mode 100644
index 00000000000..13ba8545ebe
--- /dev/null
+++ b/config/tests.webpack.preeject.js
@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+const context = require.context('../../../src', true, /\.js$/);
+context.keys()
+ .filter(path => path.match(/__tests__|\/test\/|\.(spec|test)\.js$/))
+ .forEach(context);
diff --git a/config/webpack.config.test.js b/config/webpack.config.test.js
new file mode 100644
index 00000000000..e30c0536875
--- /dev/null
+++ b/config/webpack.config.test.js
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+var webpack = require('webpack');
+var paths = require('./paths');
+
+module.exports = {
+ devtool: 'eval',
+ target: 'node',
+ entry: paths.testEntry,
+ output: {
+ path: paths.appTmp,
+ pathinfo: true,
+ filename: 'testBundle.js',
+ },
+ resolve: {
+ extensions: ['', '.js'],
+ },
+ resolveLoader: {
+ root: paths.ownNodeModules,
+ moduleTemplates: ['*-loader']
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ include: paths.appSrc,
+ loader: 'babel',
+ query: require('./babel.dev')
+ },
+ {
+ test: /\.css$/,
+ include: [paths.appSrc, paths.appNodeModules],
+ loader: 'null'
+ },
+ {
+ test: /\.json$/,
+ // include: [paths.appSrc, paths.appNodeModules],
+ loader: 'json'
+ },
+ {
+ test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)$/,
+ include: [paths.appSrc, paths.appNodeModules],
+ loader: 'file',
+ },
+ {
+ test: /\.(mp4|webm)$/,
+ include: [paths.appSrc, paths.appNodeModules],
+ loader: 'url?limit=10000'
+ }
+ ]
+ },
+ plugins: [
+ new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"test"' }),
+ // cheerio uses an implicit require('./package') that webpack doesn't understand
+ // https://github.com/cheeriojs/cheerio/issues/836
+ new webpack.NormalModuleReplacementPlugin(/^\.\/package$/, function(result) {
+ if(/cheerio/.test(result.context)) {
+ result.request = "./package.json"
+ }
+ })
+ ],
+ externals: {
+ 'react/addons': true,
+ 'react/lib/ExecutionEnvironment': true,
+ 'react/lib/ReactContext': true
+ }
+};
diff --git a/package.json b/package.json
index 321b19178ab..6ec229adfb1 100644
--- a/package.json
+++ b/package.json
@@ -38,9 +38,11 @@
"babel-preset-es2015": "6.9.0",
"babel-preset-es2016": "6.11.3",
"babel-preset-react": "6.11.1",
+ "chai": "^3.5.0",
"chalk": "1.1.3",
"cross-spawn": "4.0.0",
"css-loader": "0.23.1",
+ "enzyme": "^2.4.1",
"eslint": "3.1.1",
"eslint-loader": "1.4.1",
"eslint-plugin-import": "1.10.3",
@@ -51,8 +53,11 @@
"fs-extra": "^0.30.0",
"html-webpack-plugin": "2.22.0",
"json-loader": "0.5.4",
+ "mocha": "^2.5.3",
+ "null-loader": "^0.1.1",
"opn": "4.0.2",
"postcss-loader": "0.9.1",
+ "react-addons-test-utils": "^15.2.1",
"rimraf": "2.5.3",
"style-loader": "0.13.1",
"url-loader": "0.5.7",
diff --git a/scripts/eject.js b/scripts/eject.js
index b3d7b4ae60c..1524be65012 100644
--- a/scripts/eject.js
+++ b/scripts/eject.js
@@ -12,7 +12,6 @@ var path = require('path');
var rl = require('readline');
var rimrafSync = require('rimraf').sync;
var spawnSync = require('cross-spawn').sync;
-var paths = require('../config/paths');
var prompt = function(question, cb) {
var rlInterface = rl.createInterface({
@@ -47,11 +46,14 @@ prompt('Are you sure you want to eject? This action is permanent. [y/N]', functi
path.join('config', 'flow', 'file.js.flow'),
path.join('config', 'eslint.js'),
path.join('config', 'paths.js'),
+ path.join('config', 'tests.webpack.js'),
path.join('config', 'webpack.config.dev.js'),
path.join('config', 'webpack.config.prod.js'),
+ path.join('config', 'webpack.config.test.js'),
path.join('scripts', 'build.js'),
path.join('scripts', 'start.js'),
- path.join('scripts', 'openChrome.applescript')
+ path.join('scripts', 'openChrome.applescript'),
+ path.join('scripts', 'test.js'),
];
// Ensure that the app folder is clean and we won't override any files
diff --git a/scripts/init.js b/scripts/init.js
index a26ad799cfa..200a789533c 100644
--- a/scripts/init.js
+++ b/scripts/init.js
@@ -23,9 +23,15 @@ module.exports = function(appPath, appName, verbose, originalDirectory) {
appPackage.dependencies[key] = ownPackage.devDependencies[key];
});
+ // copy over chai and enzyme so the example test works
+ appPackage.devDependencies = appPackage.devDependencies || {};
+ ['chai', 'enzyme', 'react-addons-test-utils'].forEach(function (key) {
+ appPackage.devDependencies[key] = ownPackage.dependencies[key];
+ });
+
// Setup the script rules
appPackage.scripts = {};
- ['start', 'build', 'eject'].forEach(function(command) {
+ ['start', 'build', 'eject', 'test'].forEach(function(command) {
appPackage.scripts[command] = 'react-scripts ' + command;
});
diff --git a/scripts/test.js b/scripts/test.js
new file mode 100644
index 00000000000..9b1af349e1c
--- /dev/null
+++ b/scripts/test.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+process.env.NODE_ENV = 'test';
+
+var path = require('path');
+var rimrafSync = require('rimraf').sync;
+var webpack = require('webpack');
+var Mocha = require('mocha');
+var mocha = new Mocha();
+var paths = require('../config/paths');
+var config = require('../config/webpack.config.test');
+
+var tmpPath = paths.appTmp;
+rimrafSync(tmpPath);
+
+webpack(config).run(function(err, stats) {
+ if (err) {
+ console.error('Failed to create a test build. Reason:');
+ console.error(err.message || err);
+ process.exit(1);
+ }
+
+ mocha.addFile(path.join(tmpPath, 'testBundle.js'))
+
+ mocha.run(function(failures){
+ process.on('exit', function () {
+ process.exit(failures);
+ });
+ });
+});
diff --git a/template/gitignore b/template/gitignore
index 33ac4a7c1c8..80ad77442a3 100644
--- a/template/gitignore
+++ b/template/gitignore
@@ -6,5 +6,8 @@ node_modules
# production
build
+# test
+tmp
+
# misc
npm-debug.log
diff --git a/template/src/__tests__/App.js b/template/src/__tests__/App.js
new file mode 100644
index 00000000000..ce87ff59437
--- /dev/null
+++ b/template/src/__tests__/App.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* global describe, it */
+import React from 'react';
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import App from '../App';
+
+describe('