@@ -14,7 +14,7 @@ import {
1414 TOGGLE_MOCK_VALUE ,
1515 collectAllTexts
1616} from '../testKit'
17- import { ReactTestRenderer , act , create } from 'react-test-renderer'
17+ import { ReactTestRenderer , act , create , ReactTestInstance } from 'react-test-renderer'
1818import { AnyAction } from 'redux'
1919import { ObservedSelectorsMap , observeWithShell } from '../src'
2020
@@ -256,6 +256,155 @@ describe('connectWithShell', () => {
256256 expect ( ownPropsSpy ) . toHaveBeenCalledWith ( { ownProp : true } )
257257 } )
258258
259+ describe ( 'arePropsEqualFuncWrapper' , ( ) => {
260+ interface InnerCompDispatchProps {
261+ onClick ( ) : void
262+ }
263+ interface InnerCompOwnProps {
264+ num : number
265+ }
266+ type InnerCompProps = InnerCompOwnProps & InnerCompDispatchProps
267+ interface OuterCompStateProps {
268+ num : number
269+ str : string
270+ }
271+ type OuterCompProps = OuterCompStateProps
272+
273+ // spies
274+ let mapDispatchInnerCompSpy : jest . Mock
275+ let innerComponentRender : jest . Mock
276+ let mapStateOuterCompSpy : jest . Mock
277+ let outerComponentRenderSpy : jest . Mock
278+ let innerCompOnClickSpy : jest . Mock
279+ let innerCompShouldComponentUpdateSpy : jest . Mock
280+
281+ // action helpers
282+ let shouldUpdateInnerComp : boolean
283+ let updateOuterComp : ( newStateProps : OuterCompStateProps ) => void
284+
285+ // assertion helpers
286+ let getInnerCompText : ( ) => ReactTestInstance | string
287+ let invokeInnerCompOnClick : ( ) => void
288+
289+ beforeEach ( ( ) => {
290+ const { host, shell, renderInShellContext } = createMocks ( mockPackage )
291+
292+ // Setup - create connected inner Component
293+ innerComponentRender = jest . fn ( )
294+ mapDispatchInnerCompSpy = jest . fn ( )
295+ shouldUpdateInnerComp = false
296+ innerCompOnClickSpy = jest . fn ( num => { } )
297+ innerCompShouldComponentUpdateSpy = jest . fn ( ( ownProps : InnerCompOwnProps ) => { } )
298+
299+ const mapDispatchToProps = ( shell : Shell , state : unknown , ownProps ?: InnerCompOwnProps ) : InnerCompDispatchProps => {
300+ mapDispatchInnerCompSpy ( )
301+ return {
302+ onClick : ( ) => innerCompOnClickSpy ( ownProps ?. num || 0 )
303+ }
304+ }
305+
306+ const PureInnerComp : FunctionComponent < InnerCompProps > = ( { num, onClick } ) => {
307+ innerComponentRender ( )
308+ return < div onClick = { onClick } > { num . toString ( ) } </ div >
309+ }
310+
311+ const ConnectedInnerComp = connectWithShell ( undefined , mapDispatchToProps , shell , {
312+ shouldComponentUpdate : ( shell , nextOwnProps ) => {
313+ innerCompShouldComponentUpdateSpy ( nextOwnProps )
314+ return shouldUpdateInnerComp
315+ } ,
316+ allowOutOfEntryPoint : true
317+ } ) ( PureInnerComp )
318+
319+ // Setup - create connected outer Component
320+ mapStateOuterCompSpy = jest . fn ( )
321+ outerComponentRenderSpy = jest . fn ( )
322+
323+ let stateProps : OuterCompStateProps = { num : 1 , str : 'initialState' }
324+ const mapStateToProps = ( ) : OuterCompStateProps => {
325+ mapStateOuterCompSpy ( )
326+ return stateProps
327+ }
328+
329+ const PureOuterComp : FunctionComponent < OuterCompProps > = ( { num } ) => {
330+ outerComponentRenderSpy ( )
331+ return < ConnectedInnerComp num = { num } />
332+ }
333+
334+ const ConnectedOuterComp = connectWithShell ( mapStateToProps , undefined , shell , {
335+ allowOutOfEntryPoint : true
336+ } ) ( PureOuterComp )
337+
338+ updateOuterComp = ( newStateProps : OuterCompStateProps ) => {
339+ stateProps = newStateProps
340+
341+ act ( ( ) => {
342+ host . getStore ( ) . dispatch ( { type : '' } )
343+ host . getStore ( ) . flush ( )
344+ } )
345+ }
346+
347+ // SetUp - use a reducer that creates a new state for any dispatched action
348+ let counter = 0
349+ host . getStore ( ) . replaceReducer ( ( ) => ( {
350+ counter : ++ counter
351+ } ) )
352+
353+ // Setup - render outer component
354+ const { testKit } = renderInShellContext ( < ConnectedOuterComp /> )
355+
356+ if ( ! testKit ) {
357+ throw new Error ( 'Connected component fail to render' )
358+ }
359+
360+ // create assertion helpers
361+ getInnerCompText = ( ) => testKit . root . findByType ( ConnectedInnerComp ) . find ( x => typeof x . children [ 0 ] === 'string' ) . children [ 0 ]
362+ invokeInnerCompOnClick = ( ) =>
363+ testKit . root
364+ . findByType ( PureInnerComp )
365+ . find ( x => x . type === 'div' )
366+ . props . onClick ( )
367+ } )
368+ it ( 'should execute mapDispatchToProps, mapStateToProps, and render for both components during mount phase' , ( ) => {
369+ // Assert initial execution of mapping functions and components render
370+ expect ( mapStateOuterCompSpy ) . toHaveBeenCalledTimes ( 1 )
371+ expect ( outerComponentRenderSpy ) . toHaveBeenCalledTimes ( 1 )
372+ expect ( mapDispatchInnerCompSpy ) . toHaveBeenCalledTimes ( 1 )
373+ expect ( innerComponentRender ) . toHaveBeenCalledTimes ( 1 )
374+ expect ( getInnerCompText ( ) ) . toBe ( '1' )
375+ invokeInnerCompOnClick ( )
376+ expect ( innerCompOnClickSpy ) . toHaveBeenCalledWith ( 1 )
377+ } )
378+ it ( 'should not trigger mapDispatchToProps or re-render the inner component when ownProps change while updates are blocked for the inner component' , ( ) => {
379+ // Act - update outer component, while updates for inner component are blocked
380+ updateOuterComp ( { num : 2 , str : 'nextState_1' } )
381+
382+ // Assert - outer component re-rendered and passed new ownProps to inner component
383+ expect ( mapStateOuterCompSpy ) . toHaveBeenCalledTimes ( 2 )
384+ expect ( outerComponentRenderSpy ) . toHaveBeenCalledTimes ( 2 )
385+ expect ( innerCompShouldComponentUpdateSpy ) . toHaveBeenCalledWith ( { num : 2 } )
386+
387+ // Assert - should not trigger mapDispatchToProps or re-render of inner component even though it's ownProps have changed
388+ expect ( mapDispatchInnerCompSpy ) . toHaveBeenCalledTimes ( 1 )
389+ expect ( innerComponentRender ) . toHaveBeenCalledTimes ( 1 )
390+ } )
391+ it ( 'should trigger recalculation of mergedProps with consideration of ownProps change once updates are permitted' , ( ) => {
392+ // Act - update outer component, while updates for inner component are blocked
393+ updateOuterComp ( { num : 2 , str : 'nextState_1' } )
394+
395+ // Act - allow updates for inner component, then update outer component
396+ shouldUpdateInnerComp = true
397+ updateOuterComp ( { num : 2 , str : 'nextState_2' } )
398+
399+ // Assert - mapDispatchToProps and re-render of inner component were triggered
400+ expect ( mapDispatchInnerCompSpy ) . toHaveBeenCalledTimes ( 2 )
401+ expect ( innerComponentRender ) . toHaveBeenCalledTimes ( 2 )
402+ expect ( getInnerCompText ( ) ) . toBe ( '2' )
403+ invokeInnerCompOnClick ( )
404+ expect ( innerCompOnClickSpy ) . toHaveBeenCalledWith ( 2 )
405+ } )
406+ } )
407+
259408 it ( 'should pass scoped state to mapStateToProps' , ( ) => {
260409 const { host, shell, renderInShellContext } = createMocks ( mockPackage )
261410
0 commit comments