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 combineReducers test to typescript #3508

Merged
Merged
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
6 changes: 6 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,9 @@ export function compose<R>(
): (...args: any[]) => R

export function compose<R>(...funcs: Function[]): (...args: any[]) => R

export const __DO_NOT_USE__ActionTypes: {
INIT: string
REPLACE: string
PROBE_UNKNOWN_ACTION: () => string
}
122 changes: 79 additions & 43 deletions test/combineReducers.spec.js → test/combineReducers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,40 @@
import {
createStore,
combineReducers,
__DO_NOT_USE__ActionTypes as ActionTypes
} from '../'
Reducer,
AnyAction,
__DO_NOT_USE__ActionTypes as ActionTypes,
CombinedState
} from '..'

describe('Utils', () => {
describe('combineReducers', () => {
it('returns a composite reducer that maps the state keys to given reducers', () => {
const reducer = combineReducers({
counter: (state = 0, action) =>
counter: (state: number = 0, action) =>
action.type === 'increment' ? state + 1 : state,
stack: (state = [], action) =>
stack: (state: any[] = [], action) =>
action.type === 'push' ? [...state, action.value] : state
})

const s1 = reducer({}, { type: 'increment' })
const s1 = reducer(undefined, { type: 'increment' })
expect(s1).toEqual({ counter: 1, stack: [] })
const s2 = reducer(s1, { type: 'push', value: 'a' })
expect(s2).toEqual({ counter: 1, stack: ['a'] })
})

it('ignores all props which are not a function', () => {
// we double-cast because these conditions can only happen in a javascript setting
const reducer = combineReducers({
fake: true,
broken: 'string',
another: { nested: 'object' },
fake: (true as unknown) as Reducer,
broken: ('string' as unknown) as Reducer,
another: ({ nested: 'object' } as unknown) as Reducer,
stack: (state = []) => state
})

expect(Object.keys(reducer({}, { type: 'push' }))).toEqual(['stack'])
expect(Object.keys(reducer(undefined, { type: 'push' }))).toEqual([
'stack'
])
})

it('warns if a reducer prop is undefined', () => {
Expand All @@ -55,7 +61,7 @@ describe('Utils', () => {

it('throws an error if a reducer returns undefined handling an action', () => {
const reducer = combineReducers({
counter(state = 0, action) {
counter(state: number = 0, action) {
switch (action && action.type) {
case 'increment':
return state + 1
Expand All @@ -77,12 +83,14 @@ describe('Utils', () => {
expect(() => reducer({ counter: 0 }, null)).toThrow(
/"counter".*an action/
)
expect(() => reducer({ counter: 0 }, {})).toThrow(/"counter".*an action/)
expect(() =>
reducer({ counter: 0 }, ({} as unknown) as AnyAction)
).toThrow(/"counter".*an action/)
})

it('throws an error on first call if a reducer returns undefined initializing', () => {
const reducer = combineReducers({
counter(state, action) {
counter(state: number, action) {
switch (action.type) {
case 'increment':
return state + 1
Expand All @@ -93,7 +101,9 @@ describe('Utils', () => {
}
}
})
expect(() => reducer({})).toThrow(/"counter".*initialization/)
expect(() => reducer(undefined, { type: '' })).toThrow(
/"counter".*initialization/
)
})

it('catches error thrown in reducer when initializing and re-throw', () => {
Expand All @@ -102,14 +112,16 @@ describe('Utils', () => {
throw new Error('Error thrown in reducer')
}
})
expect(() => reducer({})).toThrow(/Error thrown in reducer/)
expect(() =>
reducer(undefined, (undefined as unknown) as AnyAction)
).toThrow(/Error thrown in reducer/)
})

it('allows a symbol to be used as an action type', () => {
const increment = Symbol('INCREMENT')

const reducer = combineReducers({
counter(state = 0, action) {
counter(state: number = 0, action) {
switch (action.type) {
case increment:
return state + 1
Expand All @@ -135,7 +147,7 @@ describe('Utils', () => {
}
})

const initialState = reducer(undefined, '@@INIT')
const initialState = reducer(undefined, { type: '@@INIT' })
expect(reducer(initialState, { type: 'FOO' })).toBe(initialState)
})

Expand All @@ -144,7 +156,7 @@ describe('Utils', () => {
child1(state = {}) {
return state
},
child2(state = { count: 0 }, action) {
child2(state: { count: number } = { count: 0 }, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
Expand All @@ -157,15 +169,15 @@ describe('Utils', () => {
}
})

const initialState = reducer(undefined, '@@INIT')
const initialState = reducer(undefined, { type: '@@INIT' })
expect(reducer(initialState, { type: 'increment' })).not.toBe(
initialState
)
})

it('throws an error on first call if a reducer attempts to handle a private action', () => {
const reducer = combineReducers({
counter(state, action) {
counter(state: number, action) {
switch (action.type) {
case 'increment':
return state + 1
Expand All @@ -179,7 +191,9 @@ describe('Utils', () => {
}
}
})
expect(() => reducer()).toThrow(/"counter".*private/)
expect(() =>
reducer(undefined, (undefined as unknown) as AnyAction)
).toThrow(/"counter".*private/)
})

it('warns if no reducers are passed to combineReducers', () => {
Expand All @@ -188,7 +202,7 @@ describe('Utils', () => {
console.error = spy

const reducer = combineReducers({})
reducer({})
reducer(undefined, { type: '' })
expect(spy.mock.calls[0][0]).toMatch(
/Store does not have a valid reducer/
)
Expand All @@ -200,9 +214,17 @@ describe('Utils', () => {
it('warns if input state does not match reducer shape', () => {
const preSpy = console.error
const spy = jest.fn()
const nullAction = (undefined as unknown) as AnyAction
console.error = spy

const reducer = combineReducers({
interface ShapeState {
foo: { bar: number }
baz: { qux: number }
}

type ShapeMismatchState = CombinedState<ShapeState>

const reducer = combineReducers<ShapeState>({
foo(state = { bar: 1 }) {
return state
},
Expand All @@ -211,44 +233,51 @@ describe('Utils', () => {
}
})

reducer()
reducer(undefined, nullAction)
expect(spy.mock.calls.length).toBe(0)

reducer({ foo: { bar: 2 } })
reducer(({ foo: { bar: 2 } } as unknown) as ShapeState, nullAction)
expect(spy.mock.calls.length).toBe(0)

reducer({
foo: { bar: 2 },
baz: { qux: 4 }
})
reducer(
{
foo: { bar: 2 },
baz: { qux: 4 }
},
nullAction
)
expect(spy.mock.calls.length).toBe(0)

createStore(reducer, { bar: 2 })
createStore(reducer, ({ bar: 2 } as unknown) as ShapeState)
expect(spy.mock.calls[0][0]).toMatch(
/Unexpected key "bar".*createStore.*instead: "foo", "baz"/
)

createStore(reducer, { bar: 2, qux: 4, thud: 5 })
createStore(reducer, ({
bar: 2,
qux: 4,
thud: 5
} as unknown) as ShapeState)
expect(spy.mock.calls[1][0]).toMatch(
/Unexpected keys "qux", "thud".*createStore.*instead: "foo", "baz"/
)

createStore(reducer, 1)
createStore(reducer, (1 as unknown) as ShapeState)
expect(spy.mock.calls[2][0]).toMatch(
/createStore has unexpected type of "Number".*keys: "foo", "baz"/
)

reducer({ corge: 2 })
reducer(({ corge: 2 } as unknown) as ShapeState, nullAction)
expect(spy.mock.calls[3][0]).toMatch(
/Unexpected key "corge".*reducer.*instead: "foo", "baz"/
)

reducer({ fred: 2, grault: 4 })
reducer(({ fred: 2, grault: 4 } as unknown) as ShapeState, nullAction)
expect(spy.mock.calls[4][0]).toMatch(
/Unexpected keys "fred", "grault".*reducer.*instead: "foo", "baz"/
)

reducer(1)
reducer((1 as unknown) as ShapeState, nullAction)
expect(spy.mock.calls[5][0]).toMatch(
/reducer has unexpected type of "Number".*keys: "foo", "baz"/
)
Expand All @@ -261,25 +290,32 @@ describe('Utils', () => {
const preSpy = console.error
const spy = jest.fn()
console.error = spy
const nullAction = { type: '' }

const foo = (state = { foo: 1 }) => state
const bar = (state = { bar: 2 }) => state

expect(spy.mock.calls.length).toBe(0)

interface WarnState {
foo: { foo: number }
bar: { bar: number }
}

const reducer = combineReducers({ foo, bar })
const state = { foo: 1, bar: 2, qux: 3 }
const state = ({ foo: 1, bar: 2, qux: 3 } as unknown) as WarnState
const bazState = ({ ...state, baz: 5 } as unknown) as WarnState

reducer(state, {})
reducer(state, {})
reducer(state, {})
reducer(state, {})
reducer(state, nullAction)
reducer(state, nullAction)
reducer(state, nullAction)
reducer(state, nullAction)
expect(spy.mock.calls.length).toBe(1)

reducer({ ...state, baz: 5 }, {})
reducer({ ...state, baz: 5 }, {})
reducer({ ...state, baz: 5 }, {})
reducer({ ...state, baz: 5 }, {})
reducer(bazState, nullAction)
reducer({ ...bazState }, nullAction)
reducer({ ...bazState }, nullAction)
reducer({ ...bazState }, nullAction)
expect(spy.mock.calls.length).toBe(2)

spy.mockClear()
Expand Down