If you are just getting started with React, please refer to the very well written documentation for React and Redux
React components can be found in app/javascript/components
.
components/
├── Header/
│ ├── index.js
│ ├── index.test.js
│ ├── Header.css
Within each component folder is the component itself index.js
, the tests index.test.js
and the required .css
files Header.css
.
We use Jest as our unit testing tool.
Enzyme is a React testing utility library. We use it for testing the output that components render as well as the wiring up of component events.
The basic steps for testing rendered output are as follows:
- Pass your component to
shallow()
to get your enzyme wrapper.
import React from 'react' // has to be in scope when we're writing JSX
import { shallow } from 'enzyme'
describe('FormField', () => {
// this vague description is just for demo purposes!
it('renders correct output', () => {
const wrapper = shallow(<FormField />)
// continued below
})
})
- Give your component all required props as normal. If you are using Component state, you can use
setState()
.
// inside your 'it' block
const wrapper = (<FormField labelText="Name" />)
wrapper.setState({ errorText: 'This field cannot be blank' })
- Test the output is correct using methods on the wrapper. You can do things like query rendered text with
text()
, and get wrappers for child elements/components withfind()
.
// still inside your 'it' block
expect(wrapper.text()).toContain('Name')
// in real life, you'd put additional
// expectations in their own 'it' blocks
const inputElement = wrapper.find('input')
expect(inputElement.prop('disabled')).toBe(true)
const errorMessageComponent = wrapper.find('ErrorMessage')
expect(errorMessageComponent.prop('text')).toBe('This field cannot be blank')
- If the behavior under test involves lifecycle methods or complex DOM interaction, pass your component to
mount()
. (The more lightweightshallow()
is preferred wherever it's usable.)
import React from 'react'
import { mount } from 'enzyme'
describe('FormField', () => {
it('reacts to events', () => {
const wrapper = mount(<FormField />)
// continued below
})
})
- Replace your component's function props with mock functions.
// inside 'it' block
const onFocus = jest.fn()
const updateFieldText = jest.fn()
const wrapper = mount(<FormField onFocus={onFocus} updateFieldText={updateFieldText} />)
- Simulate events with methods like
click()
, and trigger lifecycle events with methods likesetProps()
,setState()
,unmount()
.
const input = wrapper.find('input')
input.click()
wrapper.setState({ text: ' NonNormalizedInput ' })
- Test that your function props were called with the appropriate Jest expectations.
expect(onFocus).toHaveBeenCalled()
expect(updateFieldText).toHaveBeenCalledWith('nonnormalizedinput')
You'll notice in our Enzyme tests, we like to use setup()
functions instead of the before
/after
hooks you might see in tutorials.
This is a way to reduce duplication in tests. Especially when a component has lots of props, leaving some of them out might trigger propType warnings or break the component entirely, even if they're entirely irrelevant to the current tests. You can avoid this problem by calling shallow()
or mount()
inside of a setup()
function, and putting all required props in there.
A component that takes props directly from the Redux store via react-redux
's connect()
is called a container or a connected component.
They usually look something like this:
import React from 'react'
import { connect } from 'react-redux'
class Header extends Component {
render() {
return (
<header>
<img src={this.props.logo} />
</header>
)
}
}
const mapStateToProps = state => ({
logo: state.logoImageURL
})
export default connect(mapStateToProps)(Header)
Connected components work because we always instantiate them from within the special Provider
component from react-redux
. It's this Provider
that actually connects Header
to the Redux store. Since we give Provider
the store as a prop, it can call mapStateToProps
when the time comes and make this.props.logo
available for us to use within render
.
When we're writing unit tests, it would be a giant hassle to have to wrap every instance of Header
inside a Provider
, and to pass an entire Redux store to each Provider
. That's why we don't test the connected component itself, but rather test the Component prior to passing it to connect()
.
// add export keyword here
export class Header extends Component {
// ...
// ...
}
export default connect(mapStateToProps, mapDispatchToProps)(header)
This allows our test file to import the unwrapped class:
import { Header } from './Header'
describe('Header', () => {
it('renders a logo image', () => {
// ...
})
})
And our wrapped component can continue to be used in the application as normal!
Note that we need to disable the linter rule (import/no-named-as-default
)[import-js/eslint-plugin-import#544] when we import the default export.
import ZipEntry from './ZipEntry' // eslint-disable-line import/no-named-as-default
We use stylelint
to enforce a consistent style and best practices (such as the use of classnames in selectors over IDs and element names). You can run it locally with $ yarn stylelint
.
We use Sass. This is set up by default in Webpacker. We don't distinguish between SCSS and CSS files via extensions--use Sass syntax freely in .css
files.
We write our styles in modules in order to avoid the problems associated with CSS's global namespace. This eliminates the hassle of complex naming schemes like BEM or SMACSS.
The way this works is that we put all styles associated with a component MyComponent
inside a file MyComponent.css
like this:
.container {
position: relative;
}
.input {
border: none;
}
...and Webpack will transform those classnames in such a way that automatically prevents naming conflicts.
// final rendered HTML
<section class="MyComponent__container--xkCd42">
<input class="MyComponent__input--dLi60M" type="text">
</section>
This requires us to import the classnames like this from our Component code:
// MyComponent.js
import { container, input } from './MyComponent.css'
const MyComponent = () => (
<section className={container}>
<input className={input} type="text" />
</section>
)
export default MyComponent