-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Data: Refactor 'withDispatch' tests to use RTL #44855
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
Changes from 4 commits
1b0592a
4bfd555
2fb959a
1859ac6
0947484
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,13 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import TestRenderer, { act } from 'react-test-renderer'; | ||
| import { render, screen } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { memo } from '@wordpress/element'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
|
|
@@ -10,224 +16,157 @@ import withDispatch from '../'; | |
| import { createRegistry } from '../../../registry'; | ||
| import { RegistryProvider } from '../../registry-provider'; | ||
|
|
||
| describe( 'withDispatch', () => { | ||
| let registry; | ||
| beforeEach( () => { | ||
| registry = createRegistry(); | ||
| } ); | ||
| jest.useRealTimers(); | ||
|
|
||
| it( 'passes the relevant data to the component', () => { | ||
| const store = registry.registerStore( 'counter', { | ||
| reducer: ( state = 0, action ) => { | ||
| if ( action.type === 'increment' ) { | ||
| return state + action.count; | ||
| } | ||
| return state; | ||
| }, | ||
| actions: { | ||
| increment: ( count = 1 ) => ( { type: 'increment', count } ), | ||
| }, | ||
| describe( 'withDispatch', () => { | ||
| const storeOptions = { | ||
| reducer: ( state = 0, action ) => { | ||
| if ( action.type === 'inc' ) { | ||
| return state + action.count; | ||
| } | ||
| return state; | ||
| }, | ||
| actions: { | ||
| increment: ( count = 1 ) => ( { type: 'inc', count } ), | ||
| }, | ||
| selectors: { | ||
| getCount: ( state ) => state, | ||
| }, | ||
| }; | ||
|
|
||
| it( 'passes the relevant data to the component', async () => { | ||
| const user = userEvent.setup(); | ||
| const buttonSpy = jest.fn(); | ||
| const registry = createRegistry(); | ||
| registry.registerStore( 'counter', storeOptions ); | ||
|
|
||
| const Button = memo( ( { onClick } ) => { | ||
| buttonSpy(); | ||
| return <button onClick={ onClick } />; | ||
| } ); | ||
|
|
||
| const Component = withDispatch( ( _dispatch, ownProps ) => { | ||
| const Component = withDispatch( ( dispatch, ownProps ) => { | ||
| const { count } = ownProps; | ||
|
|
||
| return { | ||
| increment: () => { | ||
| const actionReturnedFromDispatch = Promise.resolve( | ||
| _dispatch( 'counter' ).increment( count ) | ||
| dispatch( 'counter' ).increment( count ) | ||
| ); | ||
| return expect( | ||
| actionReturnedFromDispatch | ||
| ).resolves.toEqual( { | ||
| type: 'increment', | ||
| type: 'inc', | ||
| count, | ||
| } ); | ||
|
Member
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 is not a bad thing to check: that the I would put it to the user-land code, to the async function onClick() {
const result = await props.increment();
expect( result ).toEqual( ... );
}
Member
Author
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. Restored in 1859ac6. |
||
| }, | ||
| }; | ||
| } )( ( props ) => <button onClick={ props.increment } /> ); | ||
|
|
||
| let testRenderer; | ||
| act( () => { | ||
| testRenderer = TestRenderer.create( | ||
| <RegistryProvider value={ registry }> | ||
| <Component count={ 0 } /> | ||
| </RegistryProvider> | ||
| ); | ||
| } ); | ||
| const testInstance = testRenderer.root; | ||
| } )( ( props ) => <Button onClick={ props.increment } /> ); | ||
|
|
||
| const incrementBeforeSetProps = | ||
| testInstance.findByType( 'button' ).props.onClick; | ||
| const { rerender } = render( | ||
| <RegistryProvider value={ registry }> | ||
| <Component count={ 0 } /> | ||
| </RegistryProvider> | ||
| ); | ||
|
|
||
| // Verify that dispatch respects props at the time of being invoked by | ||
| // changing props after the initial mount. | ||
| act( () => { | ||
| testRenderer.update( | ||
| <RegistryProvider value={ registry }> | ||
| <Component count={ 2 } /> | ||
| </RegistryProvider> | ||
| ); | ||
| } ); | ||
|
|
||
| // Function value reference should not have changed in props update. | ||
| expect( testInstance.findByType( 'button' ).props.onClick ).toBe( | ||
| incrementBeforeSetProps | ||
|
Member
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 is also a useful property worth testing: that the
Member
Author
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. Would something like 2fb959a work? |
||
| rerender( | ||
| <RegistryProvider value={ registry }> | ||
| <Component count={ 2 } /> | ||
| </RegistryProvider> | ||
| ); | ||
|
|
||
| act( () => { | ||
| incrementBeforeSetProps(); | ||
| } ); | ||
| // Function value reference should not have changed in props update. | ||
| // The spy method is only called during inital render. | ||
| expect( buttonSpy ).toBeCalledTimes( 1 ); | ||
|
|
||
| expect( store.getState() ).toBe( 2 ); | ||
| await user.click( screen.getByRole( 'button' ) ); | ||
| expect( registry.select( 'counter' ).getCount() ).toBe( 2 ); | ||
| } ); | ||
|
|
||
| it( 'calls dispatch on the correct registry if updated', () => { | ||
| const reducer = ( state = null ) => state; | ||
| const noop = () => ( { type: '__INERT__' } ); | ||
| const firstRegistryAction = jest.fn().mockImplementation( noop ); | ||
| const secondRegistryAction = jest.fn().mockImplementation( noop ); | ||
|
|
||
| const firstRegistry = registry; | ||
| firstRegistry.registerStore( 'demo', { | ||
| reducer, | ||
| actions: { | ||
| noop: firstRegistryAction, | ||
| }, | ||
| } ); | ||
| it( 'calls dispatch on the correct registry if updated', async () => { | ||
| const user = userEvent.setup(); | ||
|
|
||
| const Component = withDispatch( ( _dispatch ) => { | ||
| const noopByReference = _dispatch( 'demo' ).noop; | ||
| const firstRegistry = createRegistry(); | ||
| firstRegistry.registerStore( 'demo', storeOptions ); | ||
|
|
||
| const secondRegistry = createRegistry(); | ||
| secondRegistry.registerStore( 'demo', storeOptions ); | ||
|
|
||
| const Component = withDispatch( ( dispatch ) => { | ||
| return { | ||
| noop() { | ||
| _dispatch( 'demo' ).noop(); | ||
| noopByReference(); | ||
| increment() { | ||
| dispatch( 'demo' ).increment(); | ||
| }, | ||
| }; | ||
| } )( ( props ) => <button onClick={ props.noop } /> ); | ||
|
|
||
| let testRenderer; | ||
| act( () => { | ||
| testRenderer = TestRenderer.create( | ||
| <RegistryProvider value={ firstRegistry }> | ||
| <Component /> | ||
| </RegistryProvider> | ||
| ); | ||
| } ); | ||
| const testInstance = testRenderer.root; | ||
| } )( ( props ) => <button onClick={ props.increment } /> ); | ||
|
|
||
| act( () => { | ||
| testInstance.findByType( 'button' ).props.onClick(); | ||
| } ); | ||
| expect( firstRegistryAction ).toHaveBeenCalledTimes( 2 ); | ||
| expect( secondRegistryAction ).toHaveBeenCalledTimes( 0 ); | ||
| const { rerender } = render( | ||
| <RegistryProvider value={ firstRegistry }> | ||
| <Component /> | ||
| </RegistryProvider> | ||
| ); | ||
|
|
||
| const secondRegistry = createRegistry(); | ||
| secondRegistry.registerStore( 'demo', { | ||
| reducer, | ||
| actions: { | ||
| noop: secondRegistryAction, | ||
| }, | ||
| } ); | ||
| await user.click( screen.getByRole( 'button' ) ); | ||
| expect( firstRegistry.select( 'demo' ).getCount() ).toBe( 1 ); | ||
| expect( secondRegistry.select( 'demo' ).getCount() ).toBe( 0 ); | ||
|
|
||
| act( () => { | ||
| testRenderer.update( | ||
| <RegistryProvider value={ secondRegistry }> | ||
| <Component /> | ||
| </RegistryProvider> | ||
| ); | ||
| } ); | ||
| act( () => { | ||
| testInstance.findByType( 'button' ).props.onClick(); | ||
| } ); | ||
| expect( firstRegistryAction ).toHaveBeenCalledTimes( 2 ); | ||
| expect( secondRegistryAction ).toHaveBeenCalledTimes( 2 ); | ||
| rerender( | ||
| <RegistryProvider value={ secondRegistry }> | ||
| <Component /> | ||
| </RegistryProvider> | ||
| ); | ||
| await user.click( screen.getByRole( 'button' ) ); | ||
| expect( firstRegistry.select( 'demo' ).getCount() ).toBe( 1 ); | ||
| expect( secondRegistry.select( 'demo' ).getCount() ).toBe( 1 ); | ||
| } ); | ||
|
|
||
| it( | ||
| 'always calls select with the latest state in the handler passed to ' + | ||
| 'the component', | ||
| () => { | ||
| const store = registry.registerStore( 'counter', { | ||
| reducer: ( state = 0, action ) => { | ||
| if ( action.type === 'update' ) { | ||
| return action.count; | ||
| } | ||
| return state; | ||
| }, | ||
| actions: { | ||
| update: ( count ) => ( { type: 'update', count } ), | ||
| }, | ||
| selectors: { | ||
| getCount: ( state ) => state, | ||
| it( 'always calls select with the latest state in the handler passed to the component', async () => { | ||
| const user = userEvent.setup(); | ||
| const registry = createRegistry(); | ||
| registry.registerStore( 'counter', storeOptions ); | ||
|
|
||
| const Component = withDispatch( ( dispatch, ownProps, { select } ) => { | ||
| return { | ||
| update: () => { | ||
| const currentCount = select( 'counter' ).getCount(); | ||
| dispatch( 'counter' ).increment( currentCount + 1 ); | ||
| }, | ||
| } ); | ||
|
|
||
| const Component = withDispatch( | ||
| ( _dispatch, ownProps, { select: _select } ) => { | ||
| const outerCount = _select( 'counter' ).getCount(); | ||
| return { | ||
| update: () => { | ||
| const innerCount = _select( 'counter' ).getCount(); | ||
| expect( innerCount ).toBe( outerCount ); | ||
| const actionReturnedFromDispatch = Promise.resolve( | ||
| _dispatch( 'counter' ).update( innerCount + 1 ) | ||
| ); | ||
| return expect( | ||
| actionReturnedFromDispatch | ||
| ).resolves.toEqual( { | ||
| type: 'update', | ||
| count: innerCount + 1, | ||
| } ); | ||
| }, | ||
| }; | ||
| } | ||
| )( ( props ) => <button onClick={ props.update } /> ); | ||
|
|
||
| let testRenderer; | ||
| act( () => { | ||
| testRenderer = TestRenderer.create( | ||
| <RegistryProvider value={ registry }> | ||
| <Component /> | ||
| </RegistryProvider> | ||
| ); | ||
| } ); | ||
|
|
||
| const counterUpdateHandler = | ||
| testRenderer.root.findByType( 'button' ).props.onClick; | ||
|
|
||
| act( () => { | ||
| counterUpdateHandler(); | ||
| } ); | ||
| expect( store.getState() ).toBe( 1 ); | ||
|
|
||
| act( () => { | ||
| counterUpdateHandler(); | ||
| } ); | ||
| expect( store.getState() ).toBe( 2 ); | ||
|
|
||
| act( () => { | ||
| counterUpdateHandler(); | ||
| } ); | ||
| expect( store.getState() ).toBe( 3 ); | ||
| } | ||
| ); | ||
| }; | ||
| } )( ( props ) => <button onClick={ props.update } /> ); | ||
|
|
||
| render( | ||
| <RegistryProvider value={ registry }> | ||
| <Component /> | ||
| </RegistryProvider> | ||
| ); | ||
|
|
||
| await user.click( screen.getByRole( 'button' ) ); | ||
| // expectedValue = 2 * currentValue + 1. | ||
| expect( registry.select( 'counter' ).getCount() ).toBe( 1 ); | ||
|
|
||
| await user.click( screen.getByRole( 'button' ) ); | ||
| expect( registry.select( 'counter' ).getCount() ).toBe( 3 ); | ||
|
|
||
| await user.click( screen.getByRole( 'button' ) ); | ||
| expect( registry.select( 'counter' ).getCount() ).toBe( 7 ); | ||
|
Member
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 find this odd. How come from
Member
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. The test is testing whether the dispatch function can correctly read current state with
Member
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. Sorry for being unclear - I understand how it works, but I think it might be easy to miss why it's working that way. Might be a good idea to elaborate, either in a comment or in the name of the test.
Member
Author
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 will add the inline comment. |
||
| } ); | ||
|
|
||
| it( 'warns when mapDispatchToProps returns non-function property', () => { | ||
| const registry = createRegistry(); | ||
| const Component = withDispatch( () => { | ||
| return { | ||
| count: 3, | ||
| }; | ||
| } )( () => null ); | ||
|
|
||
| act( () => { | ||
| TestRenderer.create( | ||
| <RegistryProvider value={ registry }> | ||
| <Component /> | ||
| </RegistryProvider> | ||
| ); | ||
| } ); | ||
| render( | ||
| <RegistryProvider value={ registry }> | ||
| <Component /> | ||
| </RegistryProvider> | ||
| ); | ||
|
|
||
| expect( console ).toHaveWarnedWith( | ||
| 'Property count returned from dispatchMap in useDispatchWithMap must be a function.' | ||
| ); | ||
|
|
||
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.
It's also possible, and elegant, to wrap the component function itself in a spy:
The
useSelecttests use this pattern to count renders.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.
Oh, that's a neat one. Thanks, @jsnajdr!