From 5794619909481c26e90d1c77979ae294011b702d Mon Sep 17 00:00:00 2001 From: William Horton Date: Tue, 26 Jul 2016 10:29:54 -0400 Subject: [PATCH] Add testing with Mocha, Chai, and Enzyme. --- .gitignore | 1 + bin/react-scripts.js | 1 + config/paths.js | 12 ++++-- config/tests.webpack.debug.js | 13 ++++++ config/tests.webpack.js | 13 ++++++ config/tests.webpack.preeject.js | 13 ++++++ config/webpack.config.test.js | 74 ++++++++++++++++++++++++++++++++ package.json | 5 +++ scripts/eject.js | 6 ++- scripts/init.js | 8 +++- scripts/test.js | 37 ++++++++++++++++ template/gitignore | 3 ++ template/src/__tests__/App.js | 21 +++++++++ 13 files changed, 201 insertions(+), 6 deletions(-) mode change 100644 => 100755 bin/react-scripts.js create mode 100644 config/tests.webpack.debug.js create mode 100644 config/tests.webpack.js create mode 100644 config/tests.webpack.preeject.js create mode 100644 config/webpack.config.test.js create mode 100644 scripts/test.js create mode 100644 template/src/__tests__/App.js 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('', () => { + it('contains a "Welcome to React" header', () => { + const wrapper = shallow(); + expect(wrapper.contains(

Welcome to React

)).to.equal(true); + }); +});