Skip to content

Commit

Permalink
unit and integration on the frontend 👍
Browse files Browse the repository at this point in the history
  • Loading branch information
Kent C. Dodds committed Mar 9, 2017
1 parent 40d2828 commit e1f4713
Show file tree
Hide file tree
Showing 18 changed files with 465 additions and 99 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"func-style": "off",
"react/prop-types": "off",
"react/jsx-max-props-per-line": "off",
"jsx-a11y/no-marquee": "off"
"jsx-a11y/no-marquee": "off",
"no-return-assign": "off",
}
}
23 changes: 20 additions & 3 deletions client/package-scripts.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
const {series, rimraf, commonTags, crossEnv} = require('nps-utils')
const {series, rimraf, commonTags, crossEnv, concurrent} = require('nps-utils')
module.exports = {
scripts: {
dev: crossEnv('PORT=8080 react-scripts start'),
build: 'react-scripts build',
default: 'pushstate-server build',
test: {
default: 'jest --coverage',
watch: 'jest --watch',
default: concurrent.nps('test.unit', 'test.integration'),
unit: {
default: testEnv(
'jest --config=tests/jest.config.unit.json --coverage'
),
watch: testEnv('jest --config=tests/jest.config.unit.json --watch'),
},
integration: {
default: testEnv(
'jest --config=tests/jest.config.integration.json --coverage'
),
watch: testEnv(
'jest --config=tests/jest.config.integration.json --watch'
),
},
},
postinstall: {
description: commonTags.oneLine`
Expand All @@ -22,6 +35,10 @@ module.exports = {
},
}

function testEnv(script) {
return crossEnv(`NODE_ENV=test ${script}`)
}

// this is not transpiled
/*
eslint
Expand Down
26 changes: 3 additions & 23 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
"babel-jest": "^18.0.0",
"babel-preset-react-app": "^2.1.0",
"cross-env": "^3.1.4",
"enzyme": "^2.7.1",
"jest": "^18.1.0",
"nps": "^5.0.4",
"nps-utils": "^1.2.0",
"pushstate-server": "^2.2.1",
"react-addons-test-utils": "^15.4.2",
"react-scripts": "^0.9.4"
},
"license": "MIT",
Expand All @@ -31,27 +33,5 @@
"test": "nps test",
"postinstall": "nps postinstall"
},
"author": "Thinkster (https://github.com/gothinkster)",
"jest": {
"testEnvironment": "jest-environment-jsdom",
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.jsx$"
],
"collectCoverageFrom": [
"src/**"
],
"coverageThreshold": {
"global": {
"statements": 0,
"branches": 0,
"functions": 0,
"lines": 0
}
}
}
"author": "Thinkster (https://github.com/gothinkster)"
}
4 changes: 2 additions & 2 deletions client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
hashHistory,
} from 'react-router'

import store from './store'
import createStore from './store'
import App from './screens/app'
import Article from './screens/article'
import Editor from './screens/editor'
Expand All @@ -21,7 +21,7 @@ import Register from './screens/register'
import Settings from './screens/settings'

ReactDOM.render(
<Provider store={store}>
<Provider store={createStore()}>
<Router
history={
process.env.NODE_ENV === 'production' ? browserHistory : hashHistory
Expand Down
4 changes: 1 addition & 3 deletions client/src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ const promiseMiddleware = store => next => action => {
if (!skipTracking && currentState.viewChangeCounter !== currentView) {
return
}
console.log('RESULT', res)
action.payload = res
action.payload = res || {}
store.dispatch({type: 'ASYNC_END', promise: action.payload})
store.dispatch(action)
},
Expand All @@ -23,7 +22,6 @@ const promiseMiddleware = store => next => action => {
if (!skipTracking && currentState.viewChangeCounter !== currentView) {
return
}
console.log('ERROR', error)
action.error = true
action.payload = error.response.body
if (!action.skipTracking) {
Expand Down
4 changes: 1 addition & 3 deletions client/src/reducers/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default (state = {}, action) => {
export default (state = {inProgress: false, errors: null}, action) => {
switch (action.type) {
case 'LOGIN':
case 'REGISTER':
Expand All @@ -16,8 +16,6 @@ export default (state = {}, action) => {
} else {
return state
}
case 'UPDATE_FIELD_AUTH':
return {...state, [action.key]: action.value}
default:
return state
}
Expand Down
51 changes: 51 additions & 0 deletions client/src/screens/__tests__/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react'
import {mount} from 'enzyme'
import {Component as Login} from '../login'

test('renders login form by default', () => {
const wrapper = render()
expect(isSubmitButtonDisabled(wrapper)).toBe(false)
})

test('disabled button when inProgress', () => {
const wrapper = render({inProgress: true})
expect(isSubmitButtonDisabled(wrapper)).toBe(true)
})

test('shows list of errors when there are errors', () => {
const wrapper = render({
errors: {
'some-error': 'there was some error',
'some-other-error': 'there was some other error',
},
})
expect(getErrors(wrapper)).toEqual([
'some-error there was some error',
'some-other-error there was some other error',
])
})

function render(props = {}) {
const propsToUse = {
onSubmit() {},
onUnload() {},
inProgress: false,
errors: {},
...props,
}
return mount(<Login {...propsToUse} />)
}

function isSubmitButtonDisabled(wrapper) {
const button = getSubmitButton(wrapper).getNode()
return button.disabled
}

function getSubmitButton(wrapper) {
return wrapper.find('button[type="submit"]')
}

function getErrors(wrapper) {
return Array.from(wrapper.find('.error-messages li').getNodes())
.map(n => n.textContent)
}
36 changes: 16 additions & 20 deletions client/src/screens/login.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, {PropTypes} from 'react'
import {Link} from 'react-router'
import {connect} from 'react-redux'
import agent from '../shared/agent'
Expand All @@ -7,33 +7,30 @@ import ListErrors from '../shared/components/list-errors'
const mapStateToProps = state => ({...state.auth})

const mapDispatchToProps = dispatch => ({
onChangeEmail: value =>
dispatch({type: 'UPDATE_FIELD_AUTH', key: 'email', value}),
onChangePassword: value =>
dispatch({type: 'UPDATE_FIELD_AUTH', key: 'password', value}),
onSubmit: (email, password) =>
dispatch({type: 'LOGIN', payload: agent.Auth.login(email, password)}),
onUnload: () => dispatch({type: 'LOGIN_PAGE_UNLOADED'}),
})

class Login extends React.Component {
constructor() {
super()
this.changeEmail = ev => this.props.onChangeEmail(ev.target.value)
this.changePassword = ev => this.props.onChangePassword(ev.target.value)
this.submitForm = (email, password) => ev => {
ev.preventDefault()
this.props.onSubmit(email, password)
}
static propTypes = {
onSubmit: PropTypes.func.isRequired,
onUnload: PropTypes.func.isRequired,
inProgress: PropTypes.bool.isRequired,

// forwarding these props
errors: PropTypes.any,
}
submitForm = ev => {
ev.preventDefault()
this.props.onSubmit(this._email.value, this._password.value)
}

componentWillUnmount() {
this.props.onUnload()
}

render() {
const email = this.props.email
const password = this.props.password
return (
<div className="auth-page">
<div className="container page">
Expand All @@ -49,16 +46,15 @@ class Login extends React.Component {

<ListErrors errors={this.props.errors} />

<form onSubmit={this.submitForm(email, password)}>
<form onSubmit={this.submitForm}>
<fieldset>

<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="email"
placeholder="Email"
value={email}
onChange={this.changeEmail}
ref={node => this._email = node}
/>
</fieldset>

Expand All @@ -67,8 +63,7 @@ class Login extends React.Component {
className="form-control form-control-lg"
type="password"
placeholder="Password"
value={password}
onChange={this.changePassword}
ref={node => this._password = node}
/>
</fieldset>

Expand All @@ -91,4 +86,5 @@ class Login extends React.Component {
}
}

export {Login as Component}
export default connect(mapStateToProps, mapDispatchToProps)(Login)
37 changes: 11 additions & 26 deletions client/src/screens/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ import ListErrors from '../shared/components/list-errors'
const mapStateToProps = state => ({...state.auth})

const mapDispatchToProps = dispatch => ({
onChangeEmail: value =>
dispatch({type: 'UPDATE_FIELD_AUTH', key: 'email', value}),
onChangePassword: value =>
dispatch({type: 'UPDATE_FIELD_AUTH', key: 'password', value}),
onChangeUsername: value =>
dispatch({type: 'UPDATE_FIELD_AUTH', key: 'username', value}),
onSubmit: (username, email, password) => {
const payload = agent.Auth.register(username, email, password)
dispatch({type: 'REGISTER', payload})
Expand All @@ -21,26 +15,20 @@ const mapDispatchToProps = dispatch => ({
})

class Register extends React.Component {
constructor() {
super()
this.changeEmail = ev => this.props.onChangeEmail(ev.target.value)
this.changePassword = ev => this.props.onChangePassword(ev.target.value)
this.changeUsername = ev => this.props.onChangeUsername(ev.target.value)
this.submitForm = (username, email, password) => ev => {
ev.preventDefault()
this.props.onSubmit(username, email, password)
}
submitForm = ev => {
ev.preventDefault()
this.props.onSubmit(
this._username.value,
this._email.value,
this._password.value,
)
}

componentWillUnmount() {
this.props.onUnload()
}

render() {
const email = this.props.email
const password = this.props.password
const username = this.props.username

return (
<div className="auth-page">
<div className="container page">
Expand All @@ -56,16 +44,15 @@ class Register extends React.Component {

<ListErrors errors={this.props.errors} />

<form onSubmit={this.submitForm(username, email, password)}>
<form onSubmit={this.submitForm}>
<fieldset>

<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="text"
placeholder="Username"
value={this.props.username}
onChange={this.changeUsername}
ref={node => this._username = node}
data-e2e="username"
/>
</fieldset>
Expand All @@ -75,8 +62,7 @@ class Register extends React.Component {
className="form-control form-control-lg"
type="email"
placeholder="Email"
value={this.props.email}
onChange={this.changeEmail}
ref={node => this._email = node}
data-e2e="email"
/>
</fieldset>
Expand All @@ -86,8 +72,7 @@ class Register extends React.Component {
className="form-control form-control-lg"
type="password"
placeholder="Password"
value={this.props.password}
onChange={this.changePassword}
ref={node => this._password = node}
data-e2e="password"
/>
</fieldset>
Expand Down
10 changes: 7 additions & 3 deletions client/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {promiseMiddleware, localStorageMiddleware} from './middleware'
import reducer from './reducer'

const getMiddleware = () => {
if (process.env.NODE_ENV === 'production') {
const isProd = process.env.NODE_ENV === 'production'
const isTest = process.env.NODE_ENV === 'test'
if (isProd || isTest) {
return applyMiddleware(promiseMiddleware, localStorageMiddleware)
} else {
// Enable additional logging in non-production environments.
Expand All @@ -16,6 +18,8 @@ const getMiddleware = () => {
}
}

const store = createStore(reducer, getMiddleware())
export default createStoreWithState

export default store
function createStoreWithState(state = {}) {
return createStore(reducer, state, getMiddleware())
}
Loading

0 comments on commit e1f4713

Please sign in to comment.