├── .gitignore ├── README.md ├── common └── components │ └── todo-item.js ├── lib └── components │ └── todo-item.js ├── package.json └── test ├── component └── todo-item.js └── setup.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib/components/.module-cache/ 2 | 3 | # Created by https://www.gitignore.io 4 | 5 | ### vim ### 6 | [._]*.s[a-w][a-z] 7 | [._]s[a-w][a-z] 8 | *.un~ 9 | Session.vim 10 | .netrwhist 11 | *~ 12 | 13 | 14 | ### Node ### 15 | # Logs 16 | logs 17 | *.log 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directory 40 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 41 | node_modules 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *This is __Part 3__ of the series* "Modular Isomorphic React JS applications". 2 | *See [Part 1](https://github.com/jesstelford/react-isomorphic-boilerplate) and 3 | [Part 2](https://github.com/jesstelford/react-testing-mocha-jsdom) for more.* 4 | 5 | # Unit testing Isomorphic React Components 6 | 7 | **tl;dr**: *Isomorphic rendering with forms can be a painful combination. React 8 | has us covered with `refs` and `componentDidMount()`, but we still need to unit 9 | test those solutions.* 10 | 11 | As we learned in [Part 12 | 1](https://github.com/jesstelford/react-isomorphic-boilerplate), React is really 13 | powerful when used to build Isomorphic applications. Unfotunately, it has a 14 | [gotchya](https://github.com/jesstelford/react-isomorphic-boilerplate#state-change-and-slow-loading-javascript) 15 | when dealing with state change and slow loading Javascript: 16 | 17 | > When the user is on a slow connection (mobile, for example), the 18 | > `public/js/bundle.js` script file may take some time to download. During this 19 | > time, the user is already presented with the form and can begin interacting 20 | > with the checkbox. 21 | > 22 | > Unfortunately, if the user toggles the checkbox to `checked`, when React 23 | > renders the DOM, it will not detect the changed state, instead using the 24 | > passed in state as the source of truth (as it rightly should). 25 | 26 | As pointed out further in the tutorial, we can use 27 | [`refs`](https://facebook.github.io/react/docs/more-about-refs.html) and 28 | [`componentDidMount()`](https://facebook.github.io/react/docs/component-specs.html#mounting-componentdidmount) 29 | to mitigate the effects, and update the state as soon as React is done browser 30 | side rendering. 31 | 32 | But, we still need to test this aspect (our Mobile Users need to have the best 33 | possible experience too!) 34 | 35 | Continuing on from [Part 36 | 2](https://github.com/jesstelford/react-testing-mocha-jsdom), we will use 37 | [Mocha](http://mochajs.org/) + [jsdom](https://github.com/tmpvar/jsdom) to build 38 | out test cases for covering this sutation. 39 | 40 | ## Let's do it 41 | 42 | **tl;dr**: *[Get the completed 43 | example](https://github.com/jesstelford/react-testing-isormorphic)* 44 | 45 | We'll be using these libraries: 46 | 47 | * [Node.js](http://nodejs.org) 48 | * [npm](https://www.npmjs.org) 49 | * [React](https://www.npmjs.com/package/react) - ^0.12.0 50 | * [react-tools](https://www.npmjs.com/package/react-tools) - to compile JSX to JS 51 | * [Mocha](http://mochajs.org/) - testing framework and runner 52 | * [jsdom](https://github.com/tmpvar/jsdom) - headless DOM for React to use in tests 53 | 54 | Our code structure will look like this: 55 | 56 | ``` 57 | ├── common 58 | │   └── components # All our react components 59 | ├── lib 60 | │   └── components # Our jsx-compiled components 61 | └── test 62 |    └── components # Unit tests for components 63 | ``` 64 | 65 | ### `todo-item.js` React component 66 | 67 | We previously built the component `common/components/todo-item.js` in [Part 1](https://github.com/jesstelford/react-isomorphic-boilerplate#server-side-rendering): 68 | 69 | ```javascript 70 | // file: common/components/todo-item.js 71 | var React = require('react'); 72 | 73 | module.exports = React.createClass({ 74 | displayName: 'TodoItem', 75 | 76 | /** 77 | * Lifecycle functions 78 | **/ 79 | getInitialState: function() { 80 | return { done: this.props.done } 81 | }, 82 | 83 | componentDidMount: function() { 84 | this.setDone(this.refs.done.getDOMNode().checked); 85 | }, 86 | 87 | render: function() { 88 | return ( 89 | 93 | ); 94 | }, 95 | 96 | /** 97 | * Event handlers 98 | **/ 99 | onChange: function(event) { 100 | this.setDone(event.target.checked); 101 | }, 102 | 103 | /** 104 | * Utilities 105 | **/ 106 | setDone: function(done) { 107 | this.setState({ done: !!done}); 108 | } 109 | }); 110 | ``` 111 | 112 | *Notice our use of `componentDidMount()` on line 14, and our use of `refs` on 113 | lines 15 & 21* 114 | 115 | Since this component contains JSX, we must build it before we can use it by 116 | executing `./node_modules/.bin/jsx common/components/ lib/components/` (also 117 | executable via `npm run jsx` in the example repo). This will save the built file 118 | into `lib/components/todo-item.js` 119 | 120 | ### jsdom 121 | 122 | Previously, we setup jsdom with a simple DOM consisting of an empty ``, 123 | this time we want to set it up to mimic what our isomorphic server would have 124 | rendered. We can see from [Part 125 | 1](https://github.com/jesstelford/react-isomorphic-boilerplate#server-side-rendering), 126 | that it looks like this (thanks to `React.renderToString()`): 127 | 128 | ```html 129 | 130 | ``` 131 | 132 | *Remember: [space is important](https://github.com/jesstelford/react-isomorphic-boilerplate#space-is-important), don't prettify the HTML!* 133 | 134 | This gives us a final `test/setup.js` file like: 135 | 136 | ```javascript 137 | // file: test/setup.js 138 | var jsdom = require('jsdom'); 139 | 140 | // Simulating a server-side rendered component 141 | // This was obtained via React.renderToString() 142 | // Store this DOM and the window in global scope ready for React to access 143 | global.document = jsdom.jsdom(''); 144 | global.window = document.parentWindow; 145 | ``` 146 | 147 | ### A Mocha Test 148 | 149 | **tl;dr**: *Get the completed test file in the example repo at 150 | [test/component/todo-item.js](https://github.com/jesstelford/react-testing-isormorphic/blob/master/test/component/todo-item.js)* 151 | 152 | Using a similar approach to our tests in [Part 2](https://github.com/jesstelford/react-testing-mocha-jsdom#a-mocha-test), we start with what we want to test: 153 | 154 | ```javascript 155 | // file: test/component/todo-item.js 156 | var assert = require('assert'); 157 | 158 | describe('Todo-item component', function(){ 159 | 160 | it('is checked before React mount', function() { 161 | assert(this.isomorphicInputElement.checked === true); 162 | }); 163 | 164 | describe('after React mount, ', function() { 165 | 166 | it('should be checked', function() { 167 | assert(this.inputElement.checked === true); 168 | }); 169 | 170 | it('should be identical DOM element', function() { 171 | assert(this.inputElement === this.isomorphicInputElement); 172 | }); 173 | 174 | it('has checked state', function() { 175 | assert(this.renderedComponent.state.done === true); 176 | }); 177 | 178 | }); 179 | 180 | }); 181 | ``` 182 | 183 | Let's start with getting access to `this.isomorphicInputElement`. jsdom has us 184 | covered here, as we've setup the global `document` in `test/setup.js`, allowing 185 | us to query it with `getElementsByTagName`: 186 | 187 | ```javascript 188 | // file: test/component/todo-item.js 189 | var assert = require('assert'); 190 | 191 | describe('Todo-item component', function(){ 192 | 193 | before('setup DOM', function() { 194 | 195 | this.isomorphicInputElement = document.getElementsByTagName('input')[0] 196 | 197 | }); 198 | 199 | // [...] 200 | }); 201 | ``` 202 | 203 | #### Mimicing slow loading JS 204 | 205 | You'll notice in our first test, we are asserting `.checked === true`, but keep 206 | in mind when we generated the static html, the component's state is `done: 207 | false`. 208 | 209 | This is where we simulate a user having access to the DOM before the JS has 210 | finished downloading; We *check* the checkbox: 211 | 212 | ```javascript 213 | // file: test/component/todo-item.js 214 | var assert = require('assert'); 215 | 216 | describe('Todo-item component', function(){ 217 | 218 | before('setup DOM', function() { 219 | 220 | this.isomorphicInputElement = document.getElementsByTagName('input')[0] 221 | 222 | // Simulate a click on the DOM element to check the checkbox 223 | this.isomorphicInputElement.checked = true; 224 | 225 | }); 226 | 227 | // [...] 228 | }); 229 | ``` 230 | 231 | This allows our first test to run successfully; `npm test` should give output 232 | similar to: 233 | 234 | ``` 235 | Todo-item component 236 | ✓ is checked before React mount 237 | after React mount, 238 | 1) should be checked 239 | 2) has checked state 240 | 3) should be identical DOM element 241 | 242 | 243 | 1 passing (11ms) 244 | 3 failing 245 | 246 | 1) Todo-item component after React mount, should be checked: 247 | TypeError: Cannot read property 'checked' of undefined 248 | 249 | 2) Todo-item component after React mount, has checked state: 250 | TypeError: Cannot read property 'state' of undefined 251 | 252 | 3) Todo-item component after React mount, should be identical DOM element: 253 | AssertionError: false == true 254 | ``` 255 | 256 | So far, so good! 257 | 258 | #### Rendering React browser side 259 | 260 | We use an almost identical pattern as we did in [Part 261 | 2](https://github.com/jesstelford/react-testing-mocha-jsdom#a-mocha-test) (with 262 | some different variable names) to setup the rendering for React browser side (in 263 | `before('mount React', function() {`): 264 | 265 | ```javascript 266 | // file: test/component/todo-item.js 267 | var assert = require('assert'); 268 | 269 | describe('Todo-item component', function(){ 270 | 271 | before('setup DOM', function() { 272 | // [...] 273 | }); 274 | 275 | it(/* [...] */) 276 | 277 | describe('after React mount, ', function() { 278 | 279 | before('mount React', function() { 280 | 281 | // Create our component 282 | // Note that the state here and the state server side (when rendering the 283 | // isomorphic HTML) must match. This ensures the HTML React searches for 284 | // matches the HTML we have given to jsdom 285 | this.component = TodoItemFactory({ 286 | done: false, 287 | name: 'Write Tutorial' 288 | }); 289 | 290 | // We want to render into the tag 291 | this.renderTarget = document.getElementsByTagName('body')[0]; 292 | 293 | // Now, render 294 | this.renderedComponent = React.render(this.component, this.renderTarget); 295 | 296 | // Searching for tag within rendered React component 297 | // Throws an exception if not found 298 | this.inputComponent = TestUtils.findRenderedDOMComponentWithTag( 299 | this.renderedComponent, 300 | 'input' 301 | ); 302 | 303 | this.inputElement = this.inputComponent.getDOMNode(); 304 | }); 305 | 306 | it(/* [...] */) 307 | 308 | }); 309 | }); 310 | ``` 311 | 312 | With this, we are rendering React into the same `renderTarget` as the server 313 | side isomorphic render (the `` tag). We then search for the `` tag 314 | using React's `TestUtils` and store the found components in 315 | `this.renderedComponent` / `this.inputComponent` / `this.inputElement` ready for 316 | our tests to assert against. 317 | 318 | #### React's smart Virtual DOM 319 | 320 | React is smart enough (thanks to its Virtual DOM) to not wipe out our 321 | isomorphically rendered DOM element, allowing the next two tests to pass: 322 | 323 | ```javascript 324 | it('should be checked', function() { 325 | assert(this.inputElement.checked === true); 326 | }); 327 | 328 | it('should be identical DOM element', function() { 329 | assert(this.inputElement === this.isomorphicInputElement); 330 | }); 331 | ``` 332 | 333 | And, we can assert that our code in `componentDidMount()` was successfully 334 | executed by checking on the state: 335 | 336 | ```javascript 337 | it('has checked state', function() { 338 | assert(this.renderedComponent.state.done === true); 339 | }); 340 | ``` 341 | 342 | #### Conclusions 343 | 344 | With all 4 of these tests executed, we have asserted that: 345 | 346 | * The isomorphic rendered checkbox can be `checked` before the React JS has 347 | loaded and executed 348 | * Once the React JS is loaded and executed; 349 | * The DOM element is **not** erased 350 | * The React component's state is correctly updated 351 | 352 | All together now, and we end up with a complete test that can be run with 353 | `./node_modules/.bin/mocha --recursive` (alternatively can be run as `npm test` 354 | in the example repo): 355 | 356 | ```javascript 357 | // file: test/component/todo-item.js 358 | var React = require('react/addons'), 359 | assert = require('assert'), 360 | TodoItem = require('../../lib/components/todo-item'), 361 | TestUtils = React.addons.TestUtils, 362 | // Since we're not using JSX here, we need to wrap the component in a factory 363 | // manually. See https://gist.github.com/sebmarkbage/ae327f2eda03bf165261 364 | TodoItemFactory = React.createFactory(TodoItem); 365 | 366 | describe('Todo-item component', function(){ 367 | 368 | before('setup DOM', function() { 369 | 370 | this.isomorphicInputElement = document.getElementsByTagName('input')[0] 371 | 372 | // Simulate a click on the DOM element to check the checkbox 373 | this.isomorphicInputElement.checked = true; 374 | }); 375 | 376 | it('is checked before React mount', function() { 377 | assert(this.isomorphicInputElement.checked === true); 378 | }); 379 | 380 | describe('after React mount, ', function() { 381 | 382 | before('mount React', function() { 383 | 384 | // Create our component 385 | this.component = TodoItemFactory({ 386 | done: false, 387 | name: 'Write Tutorial' 388 | }); 389 | 390 | // We want to render into the tag 391 | this.renderTarget = document.getElementsByTagName('body')[0]; 392 | 393 | // Now, render 394 | this.renderedComponent = React.render(this.component, this.renderTarget); 395 | 396 | // Searching for tag within rendered React component 397 | // Throws an exception if not found 398 | this.inputComponent = TestUtils.findRenderedDOMComponentWithTag( 399 | this.renderedComponent, 400 | 'input' 401 | ); 402 | 403 | this.inputElement = this.inputComponent.getDOMNode(); 404 | }); 405 | 406 | it('should be checked', function() { 407 | assert(this.inputElement.checked === true); 408 | }); 409 | 410 | it('should be identical DOM element', function() { 411 | assert(this.inputElement === this.isomorphicInputElement); 412 | }); 413 | 414 | it('has checked state', function() { 415 | assert(this.renderedComponent.state.done === true); 416 | }); 417 | 418 | }); 419 | }); 420 | ``` 421 | 422 | ##### Results 423 | 424 | ```bash 425 | $ npm test 426 | 427 | > react-testing-mocha-jsdom@1.0.0 test /home/teddy/dev/react-mocha-jsdom 428 | > mocha --recursive 429 | 430 | 431 | 432 | Todo-item component 433 | ✓ is checked before React mount 434 | after React mount, 435 | ✓ should be checked 436 | ✓ should be identical DOM element 437 | ✓ has checked state 438 | 439 | 440 | 4 passing (23ms) 441 | ``` 442 | -------------------------------------------------------------------------------- /common/components/todo-item.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | module.exports = React.createClass({ 4 | displayName: 'TodoItem', 5 | 6 | /** 7 | * Lifecycle functions 8 | **/ 9 | getInitialState: function() { 10 | return { done: this.props.done } 11 | }, 12 | 13 | componentDidMount: function() { 14 | this.setDone(this.refs.done.getDOMNode().checked); 15 | }, 16 | 17 | render: function() { 18 | return ( 19 | 23 | ); 24 | }, 25 | 26 | /** 27 | * Event handlers 28 | **/ 29 | onChange: function(event) { 30 | this.setDone(event.target.checked); 31 | }, 32 | 33 | /** 34 | * Utilities 35 | **/ 36 | setDone: function(done) { 37 | this.setState({ done: !!done}); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /lib/components/todo-item.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | module.exports = React.createClass({ 4 | displayName: 'TodoItem', 5 | 6 | /** 7 | * Lifecycle functions 8 | **/ 9 | getInitialState: function() { 10 | return { done: this.props.done } 11 | }, 12 | 13 | componentDidMount: function() { 14 | this.setDone(this.refs.done.getDOMNode().checked); 15 | }, 16 | 17 | render: function() { 18 | return ( 19 | React.createElement("label", null, 20 | React.createElement("input", {ref: "done", type: "checkbox", defaultChecked: this.state.done, onChange: this.onChange}), 21 | this.props.name 22 | ) 23 | ); 24 | }, 25 | 26 | /** 27 | * Event handlers 28 | **/ 29 | onChange: function(event) { 30 | this.setDone(event.target.checked); 31 | }, 32 | 33 | /** 34 | * Utilities 35 | **/ 36 | setDone: function(done) { 37 | this.setState({ done: !!done}); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-testing-isomorphic", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Unit testing Isomorphic React Components", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha --recursive", 8 | "jsx": "./node_modules/.bin/jsx common/components/ lib/components/" 9 | }, 10 | "author": "Jess Telford ", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "jsdom": "^2.0.0", 14 | "mocha": "^2.1.0", 15 | "react-tools": "^0.12.1" 16 | }, 17 | "dependencies": { 18 | "react": "^0.12.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/component/todo-item.js: -------------------------------------------------------------------------------- 1 | var React = require('react/addons'), 2 | assert = require('assert'), 3 | TodoItem = require('../../lib/components/todo-item'), 4 | TestUtils = React.addons.TestUtils, 5 | // Since we're not using JSX here, we need to wrap the component in a factory 6 | // manually. See https://gist.github.com/sebmarkbage/ae327f2eda03bf165261 7 | TodoItemFactory = React.createFactory(TodoItem); 8 | 9 | describe('Todo-item component', function(){ 10 | 11 | before('setup DOM', function() { 12 | 13 | this.isomorphicInputElement = document.getElementsByTagName('input')[0] 14 | 15 | // Simulate a click on the DOM element to check the checkbox 16 | this.isomorphicInputElement.checked = true; 17 | }); 18 | 19 | it('is checked before React mount', function() { 20 | assert(this.isomorphicInputElement.checked === true); 21 | }); 22 | 23 | describe('after React mount, ', function() { 24 | 25 | before('mount React', function() { 26 | 27 | // Create our component 28 | // Note that the state here and the state server side (when rendering the 29 | // isomorphic HTML) must match. This ensures the HTML React searches for 30 | // matches the HTML we have given to jsdom 31 | this.component = TodoItemFactory({ 32 | done: false, 33 | name: 'Write Tutorial' 34 | }); 35 | 36 | // We want to render into the tag 37 | this.renderTarget = document.getElementsByTagName('body')[0]; 38 | 39 | // Now, render 40 | this.renderedComponent = React.render(this.component, this.renderTarget); 41 | 42 | // Searching for tag within rendered React component 43 | // Throws an exception if not found 44 | this.inputComponent = TestUtils.findRenderedDOMComponentWithTag( 45 | this.renderedComponent, 46 | 'input' 47 | ); 48 | 49 | this.inputElement = this.inputComponent.getDOMNode(); 50 | }); 51 | 52 | it('should be checked', function() { 53 | assert(this.inputElement.checked === true); 54 | }); 55 | 56 | it('should be identical DOM element', function() { 57 | assert(this.inputElement === this.isomorphicInputElement); 58 | }); 59 | 60 | it('has checked state', function() { 61 | assert(this.renderedComponent.state.done === true); 62 | }); 63 | 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom'); 2 | 3 | // Simulating a server-side rendered component 4 | // This was obtained via React.renderToString() 5 | // Store this DOM and the window in global scope ready for React to access 6 | global.document = jsdom.jsdom(''); 7 | global.window = document.parentWindow; 8 | --------------------------------------------------------------------------------