├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------