diff --git a/.gitignore b/.gitignore index 36d6c89..17d340a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,6 @@ ehthumbs.db Desktop.ini $RECYCLE.BIN/ node_modules/ -/test/* npm-debug.log -.idea/ \ No newline at end of file +.idea/ +/test/temp-test diff --git a/README.md b/README.md index 7872264..eec44b2 100644 --- a/README.md +++ b/README.md @@ -78,27 +78,34 @@ var Foo = React.createClass({ module.exports = Foo; ``` -And `test/spec/components/Foo.js` (*javascript - jasmine*): +And `test/spec/components/Foo.js` (*javascript - jasmine, as seen on http://simonsmith.io/unit-testing-react-components-without-a-dom/*): ```js 'use strict'; -describe('Foo', function () { - var Foo, component; +// Uncomment the following lines to use the react test utilities +// import React from 'react/addons'; +// const TestUtils = React.addons.TestUtils; - beforeEach(function () { - Foo = require('../../../src/components/Foo'); - component = Foo(); - }); +import createComponent from 'helpers/createComponent'; +import Foo from 'components/Foo.js'; - it('should create a new instance of Foo', function () { - expect(component).toBeDefined(); - }); +describe('Foo', () => { + + let FooComponent; + + beforeEach(() => { + FooComponent = createComponent(Foo); + }); + + it('should have its component name as default className', () => { + expect(FooComponent._store.props.className).toBe('Foo'); + }); }); ``` And `src/styles/Foo.css` (or .sass, .less etc...) : ```css -.Foo{ +.Foo { border: 1px dashed #f00; } ``` @@ -117,7 +124,9 @@ This will give you all of react component's most common stuff : var Foofoo = React.createClass({ mixins: [], - getInitialState: function() { return({}) }, + getInitialState: function() { + return {}; + }, getDefaultProps: function() {}, componentWillMount: function() {}, componentDidMount: function() {}, @@ -139,9 +148,6 @@ This will give you all of react component's most common stuff : Just remove those you don't need, then fill and space out the rest. - - - ### Action When using Flux or Reflux architecture, it generates an actionCreator in `src/actions` and it's corresponding test in `src/spec/actions`. @@ -364,15 +370,19 @@ Out the box the [Gruntfile](http://gruntjs.com/api/grunt.file) is configured wit 1. **webpack**: uses the [grunt-webpack](https://github.com/webpack/grunt-webpack) plugin to load all required modules and output to a single JS file `src/main.js`. This is included in the `src/index.html` file by default and will reload in the browser as and when it is recompiled. 2. **webpack-dev-server**: uses the [webpack-dev-server](https://github.com/webpack/webpack-dev-server) to watch for file changes and also serve the webpack app in development. 3. **connect**: uses the [grunt-connect](https://github.com/gruntjs/grunt-contrib-connect) plugin to start a webserver at [localhost](http://localhost:8000). -4. **karma**: uses the [grunt-karma](https://github.com/karma-runner/grunt-karma) plugin to load the Karma configuration file `karma.conf.js` located in the project root. This will run all tests using [PhantomJS](http://phantomjs.org/) by default but supports many other browsers. +4. **karma**: uses the [grunt-karma](https://github.com/karma-runner/grunt-karma) plugin to load the Karma configuration file `karma.conf.js` located in the project root. This will run all tests using [PhantomJS](http://phantomjs.org/) by default but supports many other browsers. Please note that karma-launchers other than PhantomJS must be installed separately and configured in `karma.conf.js`. ### CSS Included in the project is the [normalize.css](http://necolas.github.io/normalize.css/) script. There is also a `src/styles/main.css` script that's required by the core `src/components/App.js` component using Webpack. -### JSHint +### Linting -Please use [JSXHint](https://github.com/STRML/JSXHint) for linting JSX and the corresponding Sublime package if using SLT3 [SublimeLinter-jsxhint](https://github.com/SublimeLinter/SublimeLinter-jsxhint). Note this is a global npm install and JSX files will need to be associated with the JSX file type withing SLT3. +Webpack is automatically configured to run esLint (http://eslint.org) on every file change or build. The configuration can be found in `PROJECTROOT/.eslintrc`. There are plugins for different editors that use this tool directly: +- linter-eslint for Atom +- Sublime-Linter-eslint for Sublime + +You could also use jsxhint, the corresponding rules file is located in `PROJECTROOT/.jshintrc`. However, the support for jsxhint is planned to be dropped in a later release and only available for backwards compatibility. ## Props diff --git a/templates/common/_package.json b/templates/common/_package.json index 6e951ff..ed3064c 100644 --- a/templates/common/_package.json +++ b/templates/common/_package.json @@ -20,34 +20,33 @@ "normalize.css": "~3.0.3" }, "devDependencies": { + "babel": "^5.0.0", + "babel-loader": "^5.0.0", "grunt": "~0.4.5", "eslint": "^0.21.2", "eslint-loader": "^0.11.2", "eslint-plugin-react": "^2.4.0", "load-grunt-tasks": "~0.6.0", "grunt-contrib-connect": "~0.8.0", - "webpack": "~1.4.3", "grunt-webpack": "~1.0.8", + "jasmine-core": "^2.3.4", + "karma": "~0.12.21", + "karma-jasmine": "^0.3.5", + "karma-phantomjs-launcher": "~0.1.3", + "karma-script-launcher": "~0.1.0", + "karma-webpack": "^1.5.0", "style-loader": "~0.8.0", "url-loader": "~0.5.5", "css-loader": "~0.9.0", - "karma-script-launcher": "~0.1.0", - "karma-chrome-launcher": "~0.1.4", - "karma-firefox-launcher": "~0.1.3", - "karma-jasmine": "~0.1.5", - "karma-phantomjs-launcher": "~0.1.3", - "karma": "~0.12.21", "grunt-karma": "~0.8.3", - "karma-webpack": "~1.2.2", - "webpack-dev-server": "~1.6.5", "grunt-open": "~0.2.3", "grunt-contrib-copy": "~0.5.0", - "babel": "^4.0.0", - "babel-loader": "^4.0.0", "grunt-contrib-clean": "~0.6.0",<% if (stylesLanguage.match(/s[ac]ss/)) { %> "sass-loader": "^1.0.1",<% } %><% if (stylesLanguage === 'less') { %> "less-loader": "^2.0.0",<% } %><% if (stylesLanguage === 'stylus') { %> "stylus-loader": "^0.5.0",<% } %> - "react-hot-loader": "^1.0.7" + "react-hot-loader": "^1.0.7", + "webpack": "~1.10.0", + "webpack-dev-server": "~1.10.0" } } diff --git a/templates/common/_webpack.config.js b/templates/common/_webpack.config.js index e469bb1..84d8810 100644 --- a/templates/common/_webpack.config.js +++ b/templates/common/_webpack.config.js @@ -16,7 +16,7 @@ module.exports = { cache: true, debug: true, - devtool: false, + devtool: 'sourcemap', entry: [ 'webpack/hot/only-dev-server', './src/components/<% if (reactRouter) { %>main<% } else { %><%= scriptAppName %><% } %>.js' diff --git a/templates/common/karma.conf.js b/templates/common/karma.conf.js index 76481b4..5e0f0e4 100644 --- a/templates/common/karma.conf.js +++ b/templates/common/karma.conf.js @@ -7,12 +7,14 @@ module.exports = function (config) { basePath: '', frameworks: ['jasmine'], files: [ - 'test/helpers/**/*.js', + 'test/helpers/pack/**/*.js', + 'test/helpers/react/**/*.js', 'test/spec/components/**/*.js'<% if(architecture === 'flux'||architecture === 'reflux') { %>, 'test/spec/stores/**/*.js', 'test/spec/actions/**/*.js'<% } %> ], preprocessors: { + 'test/helpers/createComponent.js': ['webpack'], 'test/spec/components/**/*.js': ['webpack'], 'test/spec/components/**/*.jsx': ['webpack']<% if(architecture === 'flux'||architecture === 'reflux') { %>, 'test/spec/stores/**/*.js': ['webpack'], @@ -32,7 +34,8 @@ module.exports = function (config) { loader: 'url-loader?limit=10000&mimetype=image/png' }, { test: /\.(js|jsx)$/, - loader: 'babel-loader' + loader: 'babel-loader', + exclude: /node_modules/ },<% if (stylesLanguage === 'sass') { %> { test: /\.sass/, loader: 'style-loader!css-loader!sass-loader?outputStyle=expanded' @@ -61,11 +64,13 @@ module.exports = function (config) { 'styles': path.join(process.cwd(), './src/styles/'), 'components': path.join(process.cwd(), './src/components/')<% if(architecture === 'flux'||architecture === 'reflux') { %>, 'stores': '../../../src/stores/', - 'actions': '../../../src/actions/'<% } %> + 'actions': '../../../src/actions/'<% } %>, + 'helpers': path.join(process.cwd(), './test/helpers/') } } }, - webpackServer: { + webpackMiddleware: { + noInfo: true, stats: { colors: true } @@ -75,17 +80,14 @@ module.exports = function (config) { logLevel: config.LOG_INFO, colors: true, autoWatch: false, - // Start these browsers, currently available: - // - Chrome - // - ChromeCanary - // - Firefox - // - Opera - // - Safari (only Mac) - // - PhantomJS - // - IE (only Windows) browsers: ['PhantomJS'], - reporters: ['progress'], + reporters: ['dots'], captureTimeout: 60000, - singleRun: true + singleRun: true, + plugins: [ + require('karma-webpack'), + require('karma-jasmine'), + require('karma-phantomjs-launcher') + ] }); }; diff --git a/templates/common/root/.eslintrc b/templates/common/root/.eslintrc index 274a199..94df36a 100644 --- a/templates/common/root/.eslintrc +++ b/templates/common/root/.eslintrc @@ -3,7 +3,8 @@ "react" ], "ecmaFeatures": { - "jsx": true + "jsx": true, + "modules": true }, "env": { "browser": true, @@ -14,6 +15,7 @@ "quotes": [ 1, "single" ], "no-undef": false, "global-strict": false, - "no-extra-semi": 1 + "no-extra-semi": 1, + "no-underscore-dangle": false } } diff --git a/templates/common/root/test/helpers/createComponent.js b/templates/common/root/test/helpers/createComponent.js new file mode 100644 index 0000000..8a0c1da --- /dev/null +++ b/templates/common/root/test/helpers/createComponent.js @@ -0,0 +1,27 @@ +/** + * Function to get the shallow output for a given component + * As we are using phantom.js, we also need to include the fn.proto.bind shim! + * + * @see http://simonsmith.io/unit-testing-react-components-without-a-dom/ + * @author somonsmith + */ + +// Add missing methods to phantom.js +import './pack/phantomjs-shims'; + +import React from 'react/addons'; +const TestUtils = React.addons.TestUtils; + +/** + * Get the shallow rendered component + * + * @param {Object} component The component to return the output for + * @param {Object} props [optional] The components properties + * @param {Mixed} ...children [optional] List of children + * @return {Object} Shallow rendered output + */ +export default function createComponent(component, props = {}, ...children) { + const shallowRenderer = TestUtils.createRenderer(); + shallowRenderer.render(React.createElement(component, props, children.length > 1 ? children : children[0])); + return shallowRenderer.getRenderOutput(); +} diff --git a/templates/common/root/test/helpers/phantomjs-shims.js b/templates/common/root/test/helpers/pack/phantomjs-shims.js similarity index 100% rename from templates/common/root/test/helpers/phantomjs-shims.js rename to templates/common/root/test/helpers/pack/phantomjs-shims.js diff --git a/templates/javascript/App.js b/templates/javascript/App.js index 4d83182..cdddc01 100644 --- a/templates/javascript/App.js +++ b/templates/javascript/App.js @@ -12,7 +12,7 @@ var imageURL = require('../images/yeoman.png'); var <%= scriptAppName %> = React.createClass({ render: function() { return ( -
+
diff --git a/templates/javascript/Component.js b/templates/javascript/Component.js index f981129..6b4691c 100644 --- a/templates/javascript/Component.js +++ b/templates/javascript/Component.js @@ -13,7 +13,9 @@ if (stylesLanguage === 'stylus') { %>require('styles/<%= classedFileName %>.styl var <%= classedName %> = React.createClass({<% if(rich){%> mixins: [<% if(architecture === 'reflux'){%>Reflux.ListenerMixin<%}%>], - getInitialState: function() { return({}) }, + getInitialState: function() { + return {}; + }, getDefaultProps: function() {}, componentWillMount: function() {}, componentDidMount: function() {}, @@ -32,4 +34,3 @@ var <%= classedName %> = React.createClass({<% if(rich){%> <% if (es6) { %>export default <%= classedName %>;<% } else { %>module.exports = <%= classedName %>;<% } %> - diff --git a/templates/spec/Action.js b/templates/spec/Action.js index c768f48..816cf14 100644 --- a/templates/spec/Action.js +++ b/templates/spec/Action.js @@ -1,13 +1,13 @@ 'use strict'; -describe('<%= classedName %>', function() { - var action; +describe('<%= classedName %>', () => { + let action; - beforeEach(function() { + beforeEach(() => { action = require('actions/<%= classedFileName %>.js'); }); - it('should be defined', function() { + it('should be defined', () => { expect(action).toBeDefined(); }); }); diff --git a/templates/spec/App.js b/templates/spec/App.js index 5e36029..6a83c2c 100644 --- a/templates/spec/App.js +++ b/templates/spec/App.js @@ -1,11 +1,11 @@ 'use strict'; -describe('<%= classedName %>', function () { - var React = require('react/addons'); - var <%= scriptAppName %>, component; +describe('<%= classedName %>', () => { + let React = require('react/addons'); + let <%= scriptAppName %>, component; - beforeEach(function () { - var container = document.createElement('div'); + beforeEach(() => { + let container = document.createElement('div'); container.id = 'content'; document.body.appendChild(container); @@ -13,7 +13,7 @@ describe('<%= classedName %>', function () { component = React.createElement(<%= scriptAppName %>); }); - it('should create a new instance of <%= scriptAppName %>', function () { + it('should create a new instance of <%= scriptAppName %>', () => { expect(component).toBeDefined(); }); }); diff --git a/templates/spec/Component.js b/templates/spec/Component.js index adf0fb6..87f3b5b 100644 --- a/templates/spec/Component.js +++ b/templates/spec/Component.js @@ -1,15 +1,20 @@ 'use strict'; -describe('<%= classedName %>', function () { - var React = require('react/addons'); - var <%= classedName %>, component; +// Uncomment the following lines to use the react test utilities +// import React from 'react/addons'; +// const TestUtils = React.addons.TestUtils; - beforeEach(function () { - <%= classedName %> = require('components/<%= classedFileName %><%= reactComponentSuffix %>'); - component = React.createElement(<%= classedName %>); - }); +import createComponent from 'helpers/createComponent'; +import <%= classedName %> from 'components/<%= classedFileName %><%= reactComponentSuffix %>'; - it('should create a new instance of <%= classedName %>', function () { - expect(component).toBeDefined(); - }); +describe('<%= classedName %>', () => { + let <%= classedName %>Component; + + beforeEach(() => { + <%= classedName %>Component = createComponent(<%= classedName %>); + }); + + it('should have its component name as default className', () => { + expect(<%= classedName %>Component._store.props.className).toBe('<%= classedName %>'); + }); }); diff --git a/templates/spec/Store.js b/templates/spec/Store.js index a945396..3f0c9f9 100644 --- a/templates/spec/Store.js +++ b/templates/spec/Store.js @@ -1,13 +1,13 @@ 'use strict'; -describe('<%= classedName %>', function() { - var store; +describe('<%= classedName %>', () => { + let store; - beforeEach(function() { + beforeEach(() => { store = require('stores/<%= classedFileName %>.js'); }); - it('should be defined', function() { + it('should be defined', () => { expect(store).toBeDefined(); }); }); diff --git a/templates/spec/main.js b/templates/spec/main.js index 4b86f3d..1811904 100644 --- a/templates/spec/main.js +++ b/templates/spec/main.js @@ -1,14 +1,14 @@ 'use strict'; -describe('main', function () { - var main, component; +describe('main', () => { + let main, component; - beforeEach(function () { + beforeEach(() => { main = require('components/main.jsx'); component = main(); }); - it('should create a new instance of main', function () { + it('should create a new instance of main', () => { expect(component).toBeDefined(); }); }); diff --git a/test/test-creation.js b/test/test-creation.js index ca3b152..af7bbbb 100644 --- a/test/test-creation.js +++ b/test/test-creation.js @@ -85,7 +85,8 @@ describe('react-webpack generator', function() { // files not present at point of test... setTimeout(function() { helpers.assertFile([].concat(expected, [ - 'test/helpers/phantomjs-shims.js', + 'test/helpers/pack/phantomjs-shims.js', + 'test/helpers/createComponent.js', 'test/helpers/react/addons.js', 'test/spec/components/TempTestApp.js' ])); @@ -235,7 +236,6 @@ describe('react-webpack generator', function() { describe('When generating a Component', function() { var generatorTest = function(name, generatorType, specType, targetDirectory, scriptNameFn, specNameFn, suffix, done) { - var deps = [path.join('../..', generatorType)]; genOptions.appPath = 'src'; @@ -244,13 +244,11 @@ describe('react-webpack generator', function() { react.run([], function() { reactGenerator.run([], function() { helpers.assertFileContent([ - [path.join('src', targetDirectory, name + '.js'), new RegExp('var ' + scriptNameFn(name) + suffix, 'g')], [path.join('src', targetDirectory, name + '.js'), new RegExp('require\\(\'styles\\/' + name + suffix + '\\.[^\']+' + '\'\\)', 'g')], [path.join('test/spec', targetDirectory, 'TempTestApp' + '.js'), new RegExp('require\\(\'components\\/' + 'TempTestApp' + suffix + '\\.[^\']+' + '\'\\)', 'g')], - [path.join('test/spec', targetDirectory, name + '.js'), new RegExp('require\\(\'components\\/' + name + suffix + '\\.[^\']+' + '\'\\)', 'g')], + [path.join('test/spec', targetDirectory, name + '.js'), new RegExp('import ' + scriptNameFn(name) + ' from \'components\/Foo', 'g')], [path.join('test/spec', targetDirectory, name + '.js'), new RegExp('describe\\(\'' + specNameFn(name) + suffix + '\'', 'g')] - ]); done(); });