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