├── .gitignore ├── README.md ├── index.html ├── karma.conf.js ├── package.json ├── preprocessor.js ├── scripts └── bundle.js ├── server.js ├── src ├── RandomPicker.jsx ├── ScatterPlot.jsx ├── __tests__ │ ├── RandomPicker-test.jsx │ └── ScatterPlot-test.jsx └── main.jsx ├── tests.webpack.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Testing React components with Karma runner - example 2 | 3 | ![Running tests](https://draftin.com:443/images/29524?token=Pe5rMPbDhD_GwYzzq3Urg91Uj2aFI1vN8EqR_wsS5Xplty3vCrGe2MqDgS98S7iwxbIFGQBPzdRy_hWvVysQHo8) 4 | 5 | This is the example code for a companion article on testing React modules. Following this code you can take your front-end testing beyond unit tests and achieve the following: 6 | 7 | * a way to test user events 8 | * test the response to those events 9 | * make sure the right things render at the right time 10 | * run tests in many browsers 11 | * re-run tests on file changes 12 | * work with continuous integration systems 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 2 | var webpack = require('webpack'); 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | browsers: ['Chrome'], 7 | singleRun: true, 8 | frameworks: ['mocha'], 9 | files: [ 10 | 'tests.webpack.js' 11 | ], 12 | preprocessors: { 13 | 'tests.webpack.js': ['webpack'] 14 | }, 15 | reporters: ['dots'], 16 | webpack: { 17 | module: { 18 | loaders: [ 19 | {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} 20 | ] 21 | }, 22 | watch: true 23 | }, 24 | webpackServer: { 25 | noInfo: true 26 | } 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-testing-example", 3 | "version": "0.0.0", 4 | "description": "A sample project to investigate testing options with ReactJS", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "karma start" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Swizec/react-testing-example" 13 | }, 14 | "author": "Swizec Teller", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/Swizec/react-testing-example/issues" 18 | }, 19 | "homepage": "https://github.com/Swizec/react-testing-example", 20 | "devDependencies": { 21 | "babel-core": "^5.2.17", 22 | "babel-loader": "^5.0.0", 23 | "d3": "^3.5.5", 24 | "expect": "^1.6.0", 25 | "jsx-loader": "^0.13.2", 26 | "karma": "^0.12.31", 27 | "karma-chrome-launcher": "^0.1.10", 28 | "karma-cli": "0.0.4", 29 | "karma-mocha": "^0.1.10", 30 | "karma-sourcemap-loader": "^0.3.4", 31 | "karma-webpack": "^1.5.1", 32 | "mocha": "^2.2.4", 33 | "react": "^0.13.3", 34 | "react-hot-loader": "^1.2.7", 35 | "react-tools": "^0.13.3", 36 | "webpack": "^1.9.4", 37 | "webpack-dev-server": "^1.8.2" 38 | }, 39 | "jest": { 40 | "scriptPreprocessor": "/preprocessor.js", 41 | "unmockedModulePathPatterns": [ 42 | "/node_modules/react", 43 | "/node_modules/d3" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /preprocessor.js: -------------------------------------------------------------------------------- 1 | 2 | var ReactTools = require('react-tools'); 3 | 4 | module.exports = { 5 | process: function(src) { 6 | return ReactTools.transform(src); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | var webpack = require('webpack'); 3 | var WebpackDevServer = require('webpack-dev-server'); 4 | var config = require('./webpack.config'); 5 | 6 | 7 | new WebpackDevServer(webpack(config), { 8 | publicPath: config.output.publicPath, 9 | hot: true, 10 | historyApiFallback: true 11 | }).listen(3000, 'localhost', function (err, result) { 12 | if (err) { 13 | console.log(err); 14 | } 15 | 16 | console.log('Listening at localhost:3000'); 17 | }); 18 | -------------------------------------------------------------------------------- /src/RandomPicker.jsx: -------------------------------------------------------------------------------- 1 | 2 | var React = require('react/addons'); 3 | var d3 = require('d3'); 4 | 5 | var RandomPicker = React.createClass({ 6 | mixins: [React.addons.PureRenderMixin], 7 | 8 | getInitialState: function () { 9 | return { 10 | distribution: this.props.initialDistribution || "normal", 11 | count: .05, 12 | mean: 5, 13 | deviation: 1 14 | } 15 | }, 16 | 17 | inputs: { 18 | normal: [{key: "mean", label: "Mean"}, {key: "deviation", label: "Deviation"}], 19 | logNormal: [{key: "mean", label: "Mean"}, {key: "deviation", label: "Deviation"}], 20 | bates: [{key: "count", label: "Count"}], 21 | irwinHall: [{key: "count", label: "Count"}] 22 | }, 23 | 24 | pickDistribution: function (event) { 25 | this.setState({distribution: event.target.value}); 26 | }, 27 | 28 | inputChange: function (event) { 29 | var name = event.target.name, 30 | d = {}; 31 | d[name] = Number(event.target.value); 32 | 33 | this.setState(d); 34 | }, 35 | 36 | componentDidUpdate: function () { 37 | if (this.props.newRandomFunction) { 38 | var distribution = this.state.distribution, 39 | args = this.inputs[distribution].map(function (input) { 40 | return this.state[input.key] || 0; 41 | }.bind(this)); 42 | 43 | var random = d3.random[this.state.distribution].apply(this, args); 44 | 45 | this.props.newRandomFunction(random); 46 | } 47 | }, 48 | 49 | render: function () { 50 | return ( 51 |
52 |

Pick a random distribution

53 |
54 | 55 | 66 |
67 | {this.inputs[this.state.distribution].map(function (input) { 68 | return ( 69 |
70 | 71 | 76 |
77 | ); 78 | }.bind(this))} 79 |
80 | ); 81 | } 82 | }); 83 | 84 | module.exports = RandomPicker; 85 | -------------------------------------------------------------------------------- /src/ScatterPlot.jsx: -------------------------------------------------------------------------------- 1 | 2 | var React = require('react/addons'); 3 | var d3 = require('d3'); 4 | 5 | var ScatterPlot = React.createClass({ 6 | getDefaultProps: function () { 7 | return { 8 | data: [], 9 | width: 500, 10 | height: 500, 11 | point_r: 3 12 | } 13 | }, 14 | 15 | componentWillMount: function () { 16 | this.yScale = d3.scale.linear(); 17 | this.xScale = d3.scale.linear(); 18 | 19 | this.update_d3(this.props); 20 | }, 21 | 22 | componentWillReceiveProps: function (newProps) { 23 | this.update_d3(newProps); 24 | }, 25 | 26 | update_d3: function (props) { 27 | this.yScale 28 | .domain([d3.min(props.data, function (d) { return d.y; }), 29 | d3.max(props.data, function (d) { return d.y; })]) 30 | .range([props.point_r, Number(props.height-props.point_r)]); 31 | 32 | this.xScale 33 | .domain([d3.min(props.data, function (d) { return d.x; }), 34 | d3.max(props.data, function (d) { return d.x; })]) 35 | .range([props.point_r, Number(props.width-props.point_r)]); 36 | }, 37 | 38 | render: function () { 39 | return ( 40 |
41 |

This is a random scatterplot

42 | 43 | {this.props.data.map(function (pos, i) { 44 | var key = "circle-"+i; 45 | return ( 46 | 50 | ); 51 | }.bind(this))}; 52 | 53 |
54 | ); 55 | } 56 | }); 57 | 58 | module.exports = ScatterPlot; 59 | -------------------------------------------------------------------------------- /src/__tests__/RandomPicker-test.jsx: -------------------------------------------------------------------------------- 1 | 2 | var React = require('react/addons'), 3 | RandomPicker = require('../RandomPicker.jsx'), 4 | TestUtils = React.addons.TestUtils, 5 | expect = require('expect'); 6 | 7 | describe("RandomPicker", function () { 8 | it("loads without error", function () { 9 | var picker = TestUtils.renderIntoDocument( 10 | 11 | ); 12 | 13 | expect(picker).toExist(); 14 | }); 15 | 16 | it("shows two inputs for normal distribution", function () { 17 | var picker = TestUtils.renderIntoDocument( 18 | 19 | ); 20 | 21 | var input = TestUtils.scryRenderedDOMComponentsWithTag( 22 | picker, "input" 23 | ); 24 | 25 | expect(input.length).toEqual(2); 26 | }); 27 | 28 | it("shows two inputs for logNormal distribution", function () { 29 | var picker = TestUtils.renderIntoDocument( 30 | 31 | ); 32 | 33 | var input = TestUtils.scryRenderedDOMComponentsWithTag( 34 | picker, "input" 35 | ); 36 | 37 | expect(input.length).toEqual(2); 38 | }); 39 | 40 | it("shows one input for bates distribution", function () { 41 | var picker = TestUtils.renderIntoDocument( 42 | 43 | ); 44 | 45 | var input = TestUtils.scryRenderedDOMComponentsWithTag( 46 | picker, "input" 47 | ); 48 | 49 | expect(input.length).toEqual(1); 50 | }); 51 | 52 | it("shows one input for irwinHall distribution", function () { 53 | var picker = TestUtils.renderIntoDocument( 54 | 55 | ); 56 | 57 | var input = TestUtils.scryRenderedDOMComponentsWithTag( 58 | picker, "input" 59 | ); 60 | 61 | expect(input.length).toEqual(1); 62 | }); 63 | 64 | it("shows a distributions dropdown", function () { 65 | var picker = TestUtils.renderIntoDocument( 66 | 67 | ); 68 | 69 | var select = TestUtils.findRenderedDOMComponentWithTag( 70 | picker, "select" 71 | ), 72 | options = TestUtils.scryRenderedDOMComponentsWithTag( 73 | select, "option" 74 | ); 75 | 76 | expect(select).toExist(); 77 | expect(options.length).toEqual(4); 78 | }); 79 | 80 | it("changes distribution", function () { 81 | var picker = TestUtils.renderIntoDocument( 82 | 83 | ); 84 | 85 | var select = TestUtils.findRenderedDOMComponentWithTag( 86 | picker, "select" 87 | ); 88 | 89 | TestUtils.Simulate.change(select.getDOMNode(), {target: {value: "bates"}}); 90 | 91 | expect(picker.state.distribution).toEqual("bates"); 92 | }); 93 | 94 | it("saves input values", function () { 95 | var picker = TestUtils.renderIntoDocument( 96 | 97 | ), 98 | mean = TestUtils.findRenderedDOMComponentWithClass( 99 | picker, "mean" 100 | ), 101 | deviation = TestUtils.findRenderedDOMComponentWithClass( 102 | picker, "deviation" 103 | ); 104 | 105 | TestUtils.Simulate.change(mean.getDOMNode(), 106 | {target: {value: 3, name: "mean"}}); 107 | TestUtils.Simulate.change(deviation.getDOMNode(), 108 | {target: {value: 1, name: "deviation"}}); 109 | 110 | expect(picker.state.mean).toEqual(3); 111 | expect(picker.state.deviation).toEqual(1); 112 | }); 113 | 114 | it("ensures inputs are always Number", function () { 115 | var picker = TestUtils.renderIntoDocument( 116 | 117 | ); 118 | 119 | ["mean", "deviation"].forEach(function (key) { 120 | var input = TestUtils.findRenderedDOMComponentWithClass( 121 | picker, key 122 | ); 123 | 124 | TestUtils.Simulate.change(input.getDOMNode(), 125 | {target: {value: "", name: key}}); 126 | 127 | expect(picker.state[key]).toEqual(0); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /src/__tests__/ScatterPlot-test.jsx: -------------------------------------------------------------------------------- 1 | 2 | //jest.dontMock('../src/ScatterPlot.jsx'); 3 | 4 | var React = require('react/addons'), 5 | ScatterPlot = require('../ScatterPlot.jsx'), 6 | TestUtils = React.addons.TestUtils, 7 | expect = require('expect'); 8 | 9 | var d3 = require('d3'); 10 | 11 | describe('ScatterPlot', function () { 12 | var normal = d3.random.normal(1, 1), 13 | mockData = d3.range(5).map(function () { 14 | return {x: normal(), y: normal()}; 15 | }); 16 | 17 | it("renders an h1", function () { 18 | var scatterplot = TestUtils.renderIntoDocument( 19 | 20 | ); 21 | 22 | var h1 = TestUtils.findRenderedDOMComponentWithTag( 23 | scatterplot, 'h1' 24 | ); 25 | 26 | expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); 27 | }); 28 | 29 | it("renders an svg with appropriate dimensions", function () { 30 | var scatterplot = TestUtils.renderIntoDocument( 31 | 32 | ); 33 | 34 | var svg = TestUtils.findRenderedDOMComponentWithTag( 35 | scatterplot, 'svg' 36 | ); 37 | 38 | expect(svg.getDOMNode().getAttribute("width")).toEqual('500'); 39 | expect(svg.getDOMNode().getAttribute("height")).toEqual('500'); 40 | }); 41 | 42 | it("renders a circle for each datapoint", function () { 43 | var scatterplot = TestUtils.renderIntoDocument( 44 | 45 | ); 46 | 47 | var circles = TestUtils.scryRenderedDOMComponentsWithTag( 48 | scatterplot, 'circle' 49 | ); 50 | 51 | expect(circles.length).toEqual(5); 52 | }); 53 | 54 | it("keeps circles in bounds", function () { 55 | var scatterplot = TestUtils.renderIntoDocument( 56 | 57 | ); 58 | 59 | var circles = TestUtils.scryRenderedDOMComponentsWithTag( 60 | scatterplot, 'circle' 61 | ); 62 | 63 | circles.forEach(function (circle) { 64 | var cx = circle.getDOMNode().getAttribute("cx"), 65 | cy = circle.getDOMNode().getAttribute("cy"); 66 | 67 | expect(Number(cx)).toBeMoreThan(0) 68 | .toBeLessThan(500); 69 | expect(Number(cy)).toBeMoreThan(0) 70 | .toBeLessThan(500); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | 2 | var React = require('react'); 3 | var d3 = require('d3'); 4 | var ScatterPlot = require('./ScatterPlot'); 5 | var RandomPicker = require('./RandomPicker'); 6 | 7 | var App = React.createClass({ 8 | getInitialState: function () { 9 | return { 10 | random: d3.random.normal(5, 1) 11 | }; 12 | }, 13 | 14 | updateRandom: function (random) { 15 | this.setState({random: random}); 16 | }, 17 | 18 | render: function () { 19 | var data = d3.range(1000).map(function () { 20 | return {x: this.state.random(), y: this.state.random()}; 21 | }.bind(this)); 22 | 23 | return ( 24 |
25 | 26 | 27 |
28 | ); 29 | } 30 | }); 31 | 32 | React.render( 33 | , 34 | document.querySelectorAll('.container')[0] 35 | ); 36 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | 2 | var context = require.context('./src', true, /-test\.jsx?$/); 3 | context.keys().forEach(context); 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path'); 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | entry: [ 7 | 'webpack-dev-server/client?http://localhost:3000', 8 | 'webpack/hot/only-dev-server', 9 | './src/main.jsx' 10 | ], 11 | output: { 12 | filename: 'bundle.js', 13 | path: path.join(__dirname, 'build'), 14 | publicPath: '/scripts/' 15 | }, 16 | plugins: [ 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.jsx$/, 24 | loaders: ['react-hot', 'babel'], 25 | include: path.join(__dirname, 'src') 26 | } 27 | ] 28 | }, 29 | resolve: { 30 | extensions: ['', '.js', '.jsx'] 31 | } 32 | }; 33 | --------------------------------------------------------------------------------