Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert tests to react-testing-library, remove enzyme and test-renderer #996

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
630 changes: 208 additions & 422 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"build:umd:min": "cross-env BABEL_ENV=rollup NODE_ENV=production rollup -c -o dist/react-redux.min.js",
"build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd:min",
"clean": "rimraf lib dist es coverage",
"lint": "eslint src test/utils test/components test/getTestDeps.js",
"lint": "eslint src test/utils test/components",
"prepare": "npm run clean && npm run build",
"test": "node ./test/run-tests.js",
"coverage": "codecov"
Expand Down Expand Up @@ -81,18 +81,17 @@
"create-react-class": "^15.6.3",
"cross-env": "^5.2.0",
"cross-spawn": "^6.0.5",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"es3ify": "^0.2.0",
"eslint": "^4.19.1",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-react": "^7.9.1",
"glob": "^7.1.1",
"jest": "^23.4.1",
"jest-dom": "^1.12.0",
"npm-run": "^5.0.1",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-test-renderer": "^16.3.2",
"react-testing-library": "^5.0.0",
"redux": "^4.0.0",
"rimraf": "^2.6.2",
"rollup": "^0.61.1",
Expand Down
104 changes: 57 additions & 47 deletions test/components/Provider.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,28 @@ import PropTypes from 'prop-types'
import semver from 'semver'
import { createStore } from 'redux'
import { Provider, createProvider, connect } from '../../src/index.js'
import { TestRenderer, enzyme } from '../getTestDeps.js'
import * as rtl from 'react-testing-library'
Copy link
Member

@timdorr timdorr Aug 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have explicit imports here (import { render, fireEvent })? I tend to dislike import * as foo syntax out of both habit and as a very implicit signal that this is TS code (as it's a standard over there, I've noticed; a minor nit pick, for sure). Obviously it doesn't matter at all in the testing context, but it's mainly about enforcing good patterns for others in future PRs. Folks tend to copy what you do in their code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to disagree about this, not in principle, just on whether it's a good pattern. I like to do explicit imports for things that have fewer than 5 items. More than that, it gets confusing (I'm looking at you, redux-saga/effects) as to whether an imported thing is from one place or another (for example, delay is from redux-saga, the effects are not).

For react-testing-library, it is even more confusing, because render and cleanup and a few others are from the main library, but the queries like getByTextId come from the return value of render Making this clear is difficult, and I think it will reduce the learning curve for new contributors to use the * import. But I'll leave the call to you two. If you still feel it's an issue after reading my perspective, I'll change it.

import 'jest-dom/extend-expect'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be in our test setup file. (Do we have one? We should, if not.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although... we only use these files in 2 of 5 test files. so maybe not?


describe('React', () => {
describe('Provider', () => {
const createChild = (storeKey = 'store') => {
class Child extends Component {
render() {
return <div />
}
afterEach(() => rtl.cleanup())
const createChild = (storeKey = 'store') => {
class Child extends Component {
render() {
return (
<div data-testid="store">
{storeKey} - {this.context[storeKey] && this.context[storeKey].mine ? this.context[storeKey].mine : ''}
</div>
)
}
}

Child.contextTypes = {
[storeKey]: PropTypes.object.isRequired
}
Child.contextTypes = {
[storeKey]: PropTypes.object.isRequired
}

return Child
return Child
}
const Child = createChild();

Expand All @@ -34,33 +40,33 @@ describe('React', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {})

try {
expect(() => enzyme.mount(
expect(() => rtl.render(
<Provider store={store}>
<div />
</Provider>
)).not.toThrow()

if (semver.lt(React.version, '15.0.0')) {
expect(() => enzyme.mount(
expect(() => rtl.render(
<Provider store={store}>
</Provider>
)).toThrow(/children with exactly one child/)
} else {
expect(() => enzyme.mount(
expect(() => rtl.render(
<Provider store={store}>
</Provider>
)).toThrow(/a single React element child/)
}

if (semver.lt(React.version, '15.0.0')) {
expect(() => enzyme.mount(
expect(() => rtl.render(
<Provider store={store}>
<div />
<div />
</Provider>
)).toThrow(/children with exactly one child/)
} else {
expect(() => enzyme.mount(
expect(() => rtl.render(
<Provider store={store}>
<div />
<div />
Expand All @@ -75,47 +81,52 @@ describe('React', () => {

it('should add the store to the child context', () => {
const store = createStore(() => ({}))
store.mine = 'hi'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the best approach for identifying a store. It would be better to provide canary values in the initialState for each store. Setting arbitrary properties is technically possible, but it's not idiomatic Redux and might be confusing to those working with these tests in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fixing this in the 16.x PR I'm making. Fully agree, and an easy fix

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already took care of this in the #998 branch - I put some canary values in the store as suggested.


const spy = jest.spyOn(console, 'error').mockImplementation(() => {})
const testRenderer = enzyme.mount(
const tester = rtl.render(
<Provider store={store}>
<Child />
</Provider>
)
expect(spy).toHaveBeenCalledTimes(0)
spy.mockRestore()

const child = testRenderer.find(Child).instance()
expect(child.context.store).toBe(store)

expect(tester.getByTestId('store')).toHaveTextContent('store - hi')
})

it('should add the store to the child context using a custom store key', () => {
const store = createStore(() => ({}))
const CustomProvider = createProvider('customStoreKey');
const CustomChild = createChild('customStoreKey');

const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
const testRenderer = enzyme.mount(
<CustomProvider store={store}>
<CustomChild />
</CustomProvider>
)
expect(spy).toHaveBeenCalledTimes(0)
spy.mockRestore()
const store = createStore(() => ({}))
store.mine = 'hi'
const CustomProvider = createProvider('customStoreKey');
const CustomChild = createChild('customStoreKey');

const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
const tester = rtl.render(
<CustomProvider store={store}>
<CustomChild />
</CustomProvider>
)
expect(spy).toHaveBeenCalledTimes(0)
spy.mockRestore()

const child = testRenderer.find(CustomChild).instance()
expect(child.context.customStoreKey).toBe(store)
expect(tester.getByTestId('store')).toHaveTextContent('customStoreKey - hi')
})

it('should warn once when receiving a new store in props', () => {
const store1 = createStore((state = 10) => state + 1)
store1.mine = '1'
const store2 = createStore((state = 10) => state * 2)
store2.mine = '2'
const store3 = createStore((state = 10) => state * state)
store3.mine = '3'

let externalSetState
class ProviderContainer extends Component {
constructor() {
super()
this.state = { store: store1 }
externalSetState = this.setState.bind(this)
}
render() {
return (
Expand All @@ -126,14 +137,13 @@ describe('React', () => {
}
}

const testRenderer = enzyme.mount(<ProviderContainer />)
const child = testRenderer.find(Child).instance()
expect(child.context.store.getState()).toEqual(11)
const tester = rtl.render(<ProviderContainer />)
expect(tester.getByTestId('store')).toHaveTextContent('store - 1')

let spy = jest.spyOn(console, 'error').mockImplementation(() => {})
testRenderer.setState({ store: store2 })
expect(child.context.store.getState()).toEqual(11)
externalSetState({ store: store2 })

expect(tester.getByTestId('store')).toHaveTextContent('store - 1')
expect(spy).toHaveBeenCalledTimes(1)
expect(spy.mock.calls[0][0]).toBe(
'<Provider> does not support changing `store` on the fly. ' +
Expand All @@ -145,9 +155,9 @@ describe('React', () => {
spy.mockRestore()

spy = jest.spyOn(console, 'error').mockImplementation(() => {})
testRenderer.setState({ store: store3 })
expect(child.context.store.getState()).toEqual(11)
externalSetState({ store: store3 })

expect(tester.getByTestId('store')).toHaveTextContent('store - 1')
expect(spy).toHaveBeenCalledTimes(0)
spy.mockRestore()
})
Expand All @@ -168,7 +178,7 @@ describe('React', () => {
render() { return <Provider store={innerStore}><Inner /></Provider> }
}

enzyme.mount(<Provider store={outerStore}><Outer /></Provider>)
rtl.render(<Provider store={outerStore}><Outer /></Provider>)
expect(innerMapStateToProps).toHaveBeenCalledTimes(1)

innerStore.dispatch({ type: 'INC'})
Expand Down Expand Up @@ -216,7 +226,7 @@ describe('React', () => {
}
}

const testRenderer = enzyme.mount(
const tester = rtl.render(
<Provider store={store}>
<Container />
</Provider>
Expand All @@ -229,8 +239,8 @@ describe('React', () => {
expect(childMapStateInvokes).toBe(2)

// setState calls DOM handlers are batched
const button = testRenderer.find('button')
button.prop('onClick')()
const button = tester.getByText('change')
rtl.fireEvent.click(button)
expect(childMapStateInvokes).toBe(3)

// Provider uses unstable_batchedUpdates() under the hood
Expand All @@ -245,7 +255,7 @@ describe('React', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {})
const store = createStore(() => ({}))

TestRenderer.create(
rtl.render(
<React.StrictMode>
<Provider store={store}>
<div />
Expand Down
Loading