Skip to content

Commit efeadb5

Browse files
committed
Add tests to verify errors are thrown for bad mapState functions
1 parent c914580 commit efeadb5

File tree

2 files changed

+109
-1
lines changed

2 files changed

+109
-1
lines changed

test/components/connect.spec.js

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1696,7 +1696,7 @@ describe('React', () => {
16961696

16971697
it('should not error on valid component with circular structure', () => {
16981698
const createComp = Tag => {
1699-
const Comp = React.forwardRef(function Comp(props) {
1699+
const Comp = React.forwardRef(function Comp(props, ref) {
17001700
return <Tag>{props.count}</Tag>
17011701
})
17021702
Comp.__real = Comp
@@ -3111,5 +3111,112 @@ describe('React', () => {
31113111
expect(rendered.getByTestId('child').dataset.prop).toEqual('b')
31123112
})
31133113
})
3114+
3115+
it("should enforce top-down updates to ensure a deleted child's mapState doesn't throw errors", () => {
3116+
const initialState = {
3117+
a: { id: 'a', name: 'Item A' },
3118+
b: { id: 'b', name: 'Item B' },
3119+
c: { id: 'c', name: 'Item C' }
3120+
}
3121+
3122+
const reducer = (state = initialState, action) => {
3123+
switch (action.type) {
3124+
case 'DELETE_B': {
3125+
const newState = { ...state }
3126+
delete newState.b
3127+
return newState
3128+
}
3129+
default:
3130+
return state
3131+
}
3132+
}
3133+
3134+
const store = createStore(reducer)
3135+
3136+
const ListItem = ({ name }) => <div>Name: {name}</div>
3137+
3138+
let thrownError = null
3139+
3140+
const listItemMapState = (state, ownProps) => {
3141+
try {
3142+
const item = state[ownProps.id]
3143+
// If this line executes when item B has been deleted, it will throw an error.
3144+
// For this test to succeed, we should never execute mapState for item B after the item
3145+
// has been deleted, because the parent should re-render the component out of existence.
3146+
const { name } = item
3147+
return { name }
3148+
} catch (e) {
3149+
thrownError = e
3150+
}
3151+
}
3152+
3153+
const ConnectedListItem = connect(listItemMapState)(ListItem)
3154+
3155+
const appMapState = state => {
3156+
const itemIds = Object.keys(state)
3157+
return { itemIds }
3158+
}
3159+
3160+
function App({ itemIds, deleteB }) {
3161+
const items = itemIds.map(id => <ConnectedListItem key={id} id={id} />)
3162+
3163+
return (
3164+
<div className="App">
3165+
{items}
3166+
<button data-testid="deleteB">Delete B</button>
3167+
</div>
3168+
)
3169+
}
3170+
3171+
const ConnectedApp = connect(appMapState)(App)
3172+
3173+
const tester = rtl.render(
3174+
<ProviderMock store={store}>
3175+
<ConnectedApp />
3176+
</ProviderMock>
3177+
)
3178+
3179+
// This should execute without throwing an error by itself
3180+
rtl.act(() => {
3181+
store.dispatch({ type: 'DELETE_B' })
3182+
})
3183+
3184+
expect(thrownError).toBe(null)
3185+
})
3186+
3187+
it('should re-throw errors that occurred in a mapState/mapDispatch function', () => {
3188+
const counter = (state = 0, action) =>
3189+
action.type === 'INCREMENT' ? state + 1 : state
3190+
3191+
const store = createStore(counter)
3192+
3193+
const appMapState = state => {
3194+
if (state >= 1) {
3195+
throw new Error('KABOOM!')
3196+
}
3197+
3198+
return { counter: state }
3199+
}
3200+
3201+
const App = ({ counter }) => <div>Count: {counter}</div>
3202+
const ConnectedApp = connect(appMapState)(App)
3203+
3204+
const tester = rtl.render(
3205+
<ProviderMock store={store}>
3206+
<ConnectedApp />
3207+
</ProviderMock>
3208+
)
3209+
3210+
// Turn off extra console logging
3211+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {})
3212+
3213+
expect(() => {
3214+
rtl.act(() => {
3215+
store.dispatch({ type: 'INCREMENT' })
3216+
})
3217+
}).toThrow('KABOOM!')
3218+
3219+
spy.mockRestore()
3220+
})
31143221
})
31153222
})

test/integration/server-rendering.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
22
* @jest-environment node
3+
*
34
* Set this so that `window` is undefined to correctly mimic a Node SSR scenario.
45
* That allows connectAdvanced to fall back to `useEffect` instead of `useLayoutEffect`
56
* to avoid ugly console warnings when used with SSR.

0 commit comments

Comments
 (0)