├── .babelrc ├── src ├── index.js ├── 00 │ └── HelloWorld.js ├── 01 │ └── List.js ├── 02 │ └── Dropdown.js └── 03 │ └── SimpleForm.js ├── index.html ├── docs └── contributing-guidelines.md ├── .gitignore ├── webpack.config.js ├── test ├── .setup.js ├── 00-hello-world-test.js ├── 03-simple-form-test.js ├── 02-dropdown-test.js └── 01-list-test.js ├── package.json └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["airbnb"] 3 | } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Component from './01/List'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /src/00/HelloWorld.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | export default class extends Component { 4 | constructor() { 5 | super() 6 | } 7 | 8 | render() { 9 | } 10 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React TDD Exercises 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /src/01/List.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class List extends Component { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | render() { 9 | return ( 10 |
11 |
12 | ); 13 | } 14 | } 15 | 16 | export default List; 17 | -------------------------------------------------------------------------------- /src/02/Dropdown.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class Dropdown extends Component { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | render() { 9 | return ( 10 |
11 |
12 | ); 13 | } 14 | } 15 | 16 | Dropdown.propTypes = { 17 | }; 18 | 19 | export default Dropdown; 20 | -------------------------------------------------------------------------------- /docs/contributing-guidelines.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | * All tests in the test suite should be skipped 4 | * Include a gist to the solution code in the PR 5 | * Follow the naming conventions for the tests 6 | * all test files need to end with `-test.js` 7 | * test files should be enumerated and number should be in the beginning of the filename: `00-`, `01-`, `02-`... 8 | -------------------------------------------------------------------------------- /src/03/SimpleForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class SimpleForm extends Component { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | render() { 9 | return ( 10 |
11 |
12 | ); 13 | } 14 | } 15 | 16 | SimpleForm.PropTypes = { 17 | }; 18 | 19 | export default SimpleForm; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Dependency directory 18 | node_modules 19 | 20 | # Optional npm cache directory 21 | .npm 22 | 23 | # Optional REPL history 24 | .node_repl_history -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | main: './src/index.js', 4 | }, 5 | output: { 6 | path: './dist', 7 | filename: '[name].bundle.js', 8 | }, 9 | module: { 10 | loaders: [ 11 | { test: /\.js$/, exclude: /node_modules/, loaders: ['babel-loader'] }, 12 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.json', '.scss', 'css'], 17 | }, 18 | }; -------------------------------------------------------------------------------- /test/.setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register')(); 2 | 3 | var jsdom = require('jsdom').jsdom; 4 | 5 | var exposedProperties = ['window', 'navigator', 'document']; 6 | 7 | global.document = jsdom(''); 8 | global.window = document.defaultView; 9 | Object.keys(document.defaultView).forEach((property) => { 10 | if (typeof global[property] === 'undefined') { 11 | exposedProperties.push(property); 12 | global[property] = document.defaultView[property]; 13 | } 14 | }); 15 | 16 | global.navigator = { 17 | userAgent: 'node.js' 18 | }; 19 | 20 | documentRef = document; 21 | -------------------------------------------------------------------------------- /test/00-hello-world-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | import HelloWorld from '../src/00/HelloWorld'; 5 | 6 | describe("Component: Hello World", function() { 7 | 8 | xit("contains a div with class `hello-world`", function() { 9 | expect(shallow().find("div.hello-world")).to.have.length(1); 10 | }); 11 | 12 | xit("contains an h1 tag", function() { 13 | expect(shallow().find('h1')).to.have.length(1); 14 | }); 15 | 16 | xit("contains the text `Hello, World!`", function() { 17 | expect(shallow().text()).to.equal('Hello, World!'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tdd-exercises", 3 | "version": "0.1.0", 4 | "description": "React TDD exercises", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "test": "mocha test/.setup.js test/**/*-test.js", 8 | "test:watch": "mocha test/.setup.js test/**/*-test.js --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/applegrain/react-tdd-exercises.git" 13 | }, 14 | "author": "Lovisa Svallingson", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/applegrain/react-tdd-exercises/issues" 18 | }, 19 | "devDependencies": { 20 | "babel": "^6.5.2", 21 | "babel-loader": "^6.2.4", 22 | "babel-preset-airbnb": "^2.0.0", 23 | "babel-register": "^6.9.0", 24 | "chai": "^3.5.0", 25 | "css-loader": "^0.23.1", 26 | "enzyme": "2.3.0", 27 | "jsdom": "^9.4.1", 28 | "mocha": "^2.5.3", 29 | "react-addons-test-utils": "^15.2.1", 30 | "sinon": "^1.17.4", 31 | "style-loader": "^0.13.1", 32 | "update": "^0.4.2" 33 | }, 34 | "dependencies": { 35 | "babel-preset-airbnb": "^2.0.0", 36 | "jsdom": "^9.3.0", 37 | "react": "^15.1.0", 38 | "react-addons-test-utils": "^15.1.0", 39 | "react-dom": "^15.1.0", 40 | "webpack": "^1.13.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/03-simple-form-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow, mount } from 'enzyme'; 4 | import sinon from 'sinon'; 5 | import SimpleForm from '../src/03/SimpleForm'; 6 | 7 | const stub = () => {}; 8 | 9 | const setup = (customProps = {}) => { 10 | const props = Object.assign({}, { 11 | title: 'Add new song', 12 | inputFieldPlaceholder: 'Enter your favorite song...', 13 | onSubmit: stub 14 | }, customProps); 15 | 16 | return mount( ); 17 | } 18 | 19 | describe('Component: SimpleForm', () => { 20 | 21 | xit('renders the title', ()=> { 22 | const component = setup(); 23 | 24 | const title = component.props().title; 25 | expect(component.text()).to.include(title); 26 | }); 27 | 28 | xit('renders a button', ()=> { 29 | const component = setup(); 30 | 31 | expect(component.find('button')).to.have.length(1); 32 | }); 33 | 34 | xit('renders an input field and corresponding label', ()=> { 35 | const component = setup(); 36 | 37 | expect(component.find('input').props().id).to.equal('add-new-song'); 38 | expect(component.find('input').props().type).to.equal('text'); 39 | 40 | expect(component.find('label')).to.have.length(1); 41 | expect(component.find('label').props().htmlFor).to.equal('add-new-song'); 42 | expect(component.find('label').props().children).to.equal(component.props().title); 43 | }); 44 | 45 | describe('when submitted', () => { 46 | 47 | describe('and the input field is empty', ()=> { 48 | 49 | xit('there\'s a placeholder in the input field', ()=> { 50 | const component = setup(); 51 | 52 | const placeholder = component.props().inputFieldPlaceholder; 53 | expect(component.find('input').props().placeholder).to.equal(placeholder); 54 | }); 55 | 56 | xit('the button is disabled', ()=> { 57 | const component = setup(); 58 | 59 | expect(component.find('button').node.disabled).to.be.true; 60 | }); 61 | 62 | }); 63 | 64 | describe('and the input field is not empty', () => { 65 | let component, onSubmitSpy; 66 | 67 | beforeEach(() => { 68 | onSubmitSpy = sinon.spy(); 69 | component = setup({ 70 | onSubmit: onSubmitSpy, 71 | }); 72 | }); 73 | 74 | xit('the onSubmit function is called', ()=> { 75 | const value = 'Survivor'; 76 | 77 | component.find('input').simulate('change', { target: { value: value }}) 78 | component.find('button').simulate('click'); 79 | 80 | expect(onSubmitSpy.calledWith(value)).to.be.true; 81 | }); 82 | 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/02-dropdown-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow, mount } from 'enzyme'; 4 | import sinon from 'sinon'; 5 | import Dropdown from '../src/02/Dropdown'; 6 | 7 | const noop = () => {}; 8 | 9 | function setup(customProps = {}) { 10 | const items = ['item1', 'item2', 'item3']; 11 | const defaultOption = items[0]; 12 | const props = Object.assign({}, { 13 | items, 14 | value: defaultOption, 15 | onSelect: noop 16 | }, customProps); 17 | return mount( ); 18 | } 19 | 20 | describe('Component: Dropdown', () => { 21 | xit('renders a given selection', () => { 22 | const component = setup(); 23 | 24 | expect(component.find('.dropdown__toggle').text()).to.match(/item1/); 25 | }); 26 | 27 | xit('renders initially with the menu closed', () => { 28 | const component = setup(); 29 | 30 | expect(component.find('.dropdown__menu')).to.have.length(0); 31 | }); 32 | 33 | xit('renders the menu after being clicked', () => { 34 | const component = setup(); 35 | 36 | component.find('.dropdown__toggle').simulate('click'); 37 | 38 | expect(component.find('ul.dropdown__menu')).to.have.length(1); 39 | const dropdownItems = component.find('.dropdown__menu li'); 40 | expect(dropdownItems.at(0).text()).to.match(/item1/); 41 | expect(dropdownItems.at(1).text()).to.match(/item2/); 42 | expect(dropdownItems.at(2).text()).to.match(/item3/); 43 | }); 44 | 45 | describe('when an option is selected', () => { 46 | let component, onSelectSpy; 47 | beforeEach(() => { 48 | onSelectSpy = sinon.spy(); 49 | component = setup({ 50 | onSelect: onSelectSpy, 51 | }); 52 | component.find('.dropdown__toggle').simulate('click'); 53 | component.find('.dropdown__menu li').at(1).simulate('click'); 54 | }); 55 | 56 | xit('sets the dropdown\'s value', () => { 57 | expect(component.find('.dropdown__toggle').text()).to.match(/item2/); 58 | }); 59 | 60 | xit('closes the menu', () => { 61 | expect(component.find('.dropdown__menu')).to.have.length(0); 62 | }); 63 | 64 | xit('fires a callback with the correct arguments', () => { 65 | expect(onSelectSpy.calledWith('item2')).to.be.true; 66 | }); 67 | }); 68 | 69 | xit('doesn\'t fire a callback when a user re-picks the currently selected option', () => { 70 | const onSelectSpy = sinon.spy(); 71 | const component = setup({ 72 | onSelect: onSelectSpy, 73 | }); 74 | 75 | component.find('.dropdown__toggle').simulate('click'); 76 | component.find('.dropdown__menu li').at(0).simulate('click'); 77 | 78 | expect(onSelectSpy.calledOnce).to.be.false; 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React TDD examples 2 | 3 | A repository with test suites for React components. 4 | 5 | Testing tools: [Enzyme](http://airbnb.io/enzyme/), [Mocha](https://mochajs.org/), [Chai](http://chaijs.com/) 6 | 7 | - [Contributing Guidelines](docs/contributing-guidelines.md) :gift_heart: 8 | - [Up and running](https://github.com/applegrain/react-tdd-exercises/blob/master/README.md#up-and-running) 9 | - [How-to](https://github.com/applegrain/react-tdd-exercises/blob/master/README.md#how-to) 10 | - [Running the tests](https://github.com/applegrain/react-tdd-exercises/blob/master/README.md#running-the-tests) 11 | - [Browser debugging](https://github.com/applegrain/react-tdd-exercises/blob/master/README.md#browser-debugging) 12 | - [About the tools](https://github.com/applegrain/react-tdd-exercises/blob/master/README.md#about-the-tools) 13 | 14 | --- 15 | 16 | ## Up and running 17 | 18 | In your terminal, clone the repository and install the dependencies: 19 | 20 | ```sh 21 | $ git clone git@github.com:applegrain/react-tdd-exercises.git 22 | $ cd react-tdd-exercises 23 | $ npm install 24 | ``` 25 | 26 | --- 27 | 28 | ## How-to 29 | 30 | In `./test` there are enumerated test suites, and every test in the suites are skipped. Work with one test suite at a time, starting with `00-hello-world`. Unskip one test at a time and write code to make it pass. 31 | 32 | --- 33 | 34 | ## Running the tests 35 | 36 | To run the entire test suite: 37 | 38 | ```sh 39 | $ npm test 40 | ``` 41 | 42 | To run a specific test, chain the `.only()` function call to your `it` block. 43 | 44 | ```javascript 45 | it.only("contains a div with class `hello-world`", function() { 46 | expect(shallow().contains(
)).to.equal(true); 47 | }); 48 | ``` 49 | 50 | The same applies for `describe` block: 51 | 52 | ```javascript 53 | describe.only("Component: Hello World", function() { 54 | 55 | ... 56 | 57 | }); 58 | ``` 59 | 60 | To exclude some tests or test suites from running with the entire test suite, add the `.skip()` call to either `it` or `describe` blocks. 61 | 62 | You can also add prepend `it` and `describe` blocks with an `x`: 63 | 64 | ```javascript 65 | describe.only("Component: Hello World", function() { 66 | 67 | xit.only("contains a div with class `hello-world`", function() { 68 | expect(shallow().contains(
)).to.equal(true); 69 | }); 70 | 71 | }); 72 | ``` 73 | 74 | --- 75 | 76 | ## Browser debugging 77 | 78 | If you want to debug your component from the browser, open `src/index.js` and change the imported component on line 4 to the one you are working with. Then, run `webpack` from your browser to bundle your code, and then open `index.html`. 79 | 80 | From your terminal, run: 81 | 82 | ```sh 83 | $ webpack 84 | $ open index.html 85 | ``` 86 | 87 | --- 88 | 89 | ## About the tools 90 | 91 | * [React](https://facebook.github.io/react/): a rendering library 92 | * [Enzyme](http://airbnb.io/enzyme/): a testing utility library for React 93 | * [Mocha](https://mochajs.org/): a JavaScript testing framework 94 | * [Chai](http://chaijs.com/): a JavaScript assertion library 95 | -------------------------------------------------------------------------------- /test/01-list-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow, mount } from 'enzyme'; 4 | import List from '../src/01/List'; 5 | 6 | describe("Component: List", function() { 7 | 8 | xit("contains a div with class `content`", function() { 9 | expect(shallow().find("div.content")).to.have.length(1); 10 | }); 11 | 12 | xit("has an input field", function() { 13 | expect(shallow().find("input")).to.have.length(1); 14 | }); 15 | 16 | xit("has a button with text `Submit`", function() { 17 | const wrapper = shallow(); 18 | expect(wrapper.find("button")).to.have.length(1); 19 | expect(wrapper.find("button").first().text()).to.equal("Submit"); 20 | }); 21 | 22 | xit("has a state `names` which is initialized as an empty array", function() { 23 | const wrapper = shallow(); 24 | expect(wrapper.state().names).to.be.instanceOf(Array); 25 | expect(wrapper.state().names.length).to.equal(0); 26 | }); 27 | 28 | xit("has a state `current` which is initialized as an empty string", function() { 29 | const wrapper = shallow(); 30 | expect(wrapper.state().current).to.equal(""); 31 | }); 32 | 33 | xit("the input field has an onChange event listener", function() { 34 | const inputField = shallow().find("input"); 35 | expect(inputField.props().onChange).to.be.instanceOf(Function); 36 | }); 37 | 38 | xit("submitting text in the input field adds to the state", function() { 39 | const wrapper = mount(); 40 | const name = "Mariko"; 41 | wrapper.setState({ current: name }) 42 | 43 | wrapper.find("button").simulate("click"); 44 | 45 | expect(wrapper.state().names.length).to.equal(1); 46 | expect(wrapper.state().names[0]).to.equal(name); 47 | }); 48 | 49 | xit("given state `current` is empty, it renders an h1", function() { 50 | const wrapper = mount(); 51 | 52 | expect(shallow().find("div.content")).to.have.length(1); 53 | expect(wrapper.find("div.content")).to.have.length(1); 54 | expect(wrapper.find("div.content").text()).to.include("Nothing here yet :("); 55 | }); 56 | 57 | xit("renders submitted text", function() { 58 | const wrapper = mount(); 59 | const name = "Mariko"; 60 | wrapper.setState({ names: [name] }) 61 | 62 | expect(shallow().find("div.content")).to.have.length(1); 63 | expect(wrapper.find("div.content")).to.have.length(1); 64 | 65 | expect(wrapper.find("div.content").text()).to.not.include("Nothing here yet :("); 66 | expect(wrapper.state().names.length).to.equal(1); 67 | expect(wrapper.find("div.content").text()).to.include(name); 68 | }); 69 | 70 | xit("renders all submitted text", function() { 71 | const wrapper = mount(); 72 | let name = "Mariko"; 73 | wrapper.setState({ names: [name] }) 74 | 75 | expect(shallow().find("div.content")).to.have.length(1); 76 | expect(wrapper.find("div.content")).to.have.length(1); 77 | 78 | expect(wrapper.find("div.content").text()).to.not.include("Nothing here yet :("); 79 | expect(wrapper.state().names.length).to.equal(1); 80 | expect(wrapper.find("div.content").text()).to.include(name); 81 | 82 | let name2 = "Ted"; 83 | wrapper.setState({ names: [name, name2] }); 84 | 85 | expect(wrapper.find("div.content").text()).to.include(name); 86 | expect(wrapper.find("div.content").text()).to.include(name2); 87 | }); 88 | }); 89 | --------------------------------------------------------------------------------