-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Changes from all commits
aaa0470
41ff984
9f44bed
865cc82
eaa3284
499aa63
0e2e194
1e0ad89
545f53d
5333b40
a47b64c
77d4fa3
0f427b5
b255a46
eb960c4
9578017
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
import 'jest-dom/extend-expect' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agreed There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
||
|
@@ -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 /> | ||
|
@@ -75,47 +81,52 @@ describe('React', () => { | |
|
||
it('should add the store to the child context', () => { | ||
const store = createStore(() => ({})) | ||
store.mine = 'hi' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ( | ||
|
@@ -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. ' + | ||
|
@@ -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() | ||
}) | ||
|
@@ -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'}) | ||
|
@@ -216,7 +226,7 @@ describe('React', () => { | |
} | ||
} | ||
|
||
const testRenderer = enzyme.mount( | ||
const tester = rtl.render( | ||
<Provider store={store}> | ||
<Container /> | ||
</Provider> | ||
|
@@ -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 | ||
|
@@ -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 /> | ||
|
There was a problem hiding this comment.
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 dislikeimport * 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.There was a problem hiding this comment.
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
andcleanup
and a few others are from the main library, but the queries likegetByTextId
come from the return value ofrender
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.