Skip to content

Commit

Permalink
convert combineReducers test
Browse files Browse the repository at this point in the history
  • Loading branch information
cellog committed Aug 16, 2019
1 parent 76d1f98 commit 3b87f60
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 45 deletions.
10 changes: 9 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,9 @@ export interface Store<S = any, A extends Action = AnyAction> {
*
* @param nextReducer The reducer for the store to use instead.
*/
replaceReducer(nextReducer: Reducer<S, A>): void
replaceReducer<NewState = S, NewActions extends A = A>(
nextReducer: Reducer<NewState, NewActions>
): Store<NewState, NewActions>

/**
* Interoperability point for observable/reactive libraries.
Expand Down Expand Up @@ -667,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
}
4 changes: 3 additions & 1 deletion src/createStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export default function createStore(reducer, preloadedState, enhancer) {
// will receive the previous state. This effectively populates
// the new state tree with any relevant data from the old one.
dispatch({ type: ActionTypes.REPLACE })
return store
}

/**
Expand Down Expand Up @@ -284,11 +285,12 @@ export default function createStore(reducer, preloadedState, enhancer) {
// the initial state tree.
dispatch({ type: ActionTypes.INIT })

return {
const store = {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
return store
}
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

0 comments on commit 3b87f60

Please sign in to comment.