From 0ddaa95d0e0d254679d4dc9e06ffa72a9ac7110f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 30 Jan 2019 11:13:36 +0100 Subject: [PATCH] Added reducerFactory and tests --- .../core/redux/actionCreatorFactory.test.ts | 2 +- public/app/core/redux/reducerFactory.test.ts | 99 +++++++++++++++++++ public/app/core/redux/reducerFactory.ts | 55 +++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 public/app/core/redux/reducerFactory.test.ts create mode 100644 public/app/core/redux/reducerFactory.ts diff --git a/public/app/core/redux/actionCreatorFactory.test.ts b/public/app/core/redux/actionCreatorFactory.test.ts index f79c1ebe0dc..18ba6915368 100644 --- a/public/app/core/redux/actionCreatorFactory.test.ts +++ b/public/app/core/redux/actionCreatorFactory.test.ts @@ -11,7 +11,7 @@ interface Dummy { b: boolean; } -const setup = payload => { +const setup = (payload: Dummy) => { resetAllActionCreatorTypes(); const actionCreator = actionCreatorFactory('dummy').create(); const result = actionCreator(payload); diff --git a/public/app/core/redux/reducerFactory.test.ts b/public/app/core/redux/reducerFactory.test.ts new file mode 100644 index 00000000000..85b3939efad --- /dev/null +++ b/public/app/core/redux/reducerFactory.test.ts @@ -0,0 +1,99 @@ +import { reducerFactory } from './reducerFactory'; +import { actionCreatorFactory, GrafanaAction } from './actionCreatorFactory'; + +interface DummyReducerState { + n: number; + s: string; + b: boolean; + o: { + n: number; + s: string; + b: boolean; + }; +} + +const dummyReducerIntialState: DummyReducerState = { + n: 1, + s: 'One', + b: true, + o: { + n: 2, + s: 'two', + b: false, + }, +}; + +const dummyActionCreator = actionCreatorFactory('dummy').create(); + +const dummyReducer = reducerFactory(dummyReducerIntialState) + .addHandler({ + creator: dummyActionCreator, + handler: ({ state, action }) => { + return { ...state, ...action.payload }; + }, + }) + .create(); + +describe('reducerFactory', () => { + describe('given it is created with a defined handler', () => { + describe('when reducer is called with no state', () => { + describe('and with an action that the handler can not handle', () => { + it('then the resulting state should be intial state', () => { + const result = dummyReducer(undefined as DummyReducerState, {} as GrafanaAction); + + expect(result).toEqual(dummyReducerIntialState); + }); + }); + + describe('and with an action that the handler can handle', () => { + it('then the resulting state should correct', () => { + const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } }; + const result = dummyReducer(undefined as DummyReducerState, dummyActionCreator(payload)); + + expect(result).toEqual(payload); + }); + }); + }); + + describe('when reducer is called with a state', () => { + describe('and with an action that the handler can not handle', () => { + it('then the resulting state should be intial state', () => { + const result = dummyReducer(dummyReducerIntialState, {} as GrafanaAction); + + expect(result).toEqual(dummyReducerIntialState); + }); + }); + + describe('and with an action that the handler can handle', () => { + it('then the resulting state should correct', () => { + const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } }; + const result = dummyReducer(dummyReducerIntialState, dummyActionCreator(payload)); + + expect(result).toEqual(payload); + }); + }); + }); + }); + + describe('given a handler is added', () => { + describe('when a handler with the same creator is added', () => { + it('then is should throw', () => { + const faultyReducer = reducerFactory(dummyReducerIntialState).addHandler({ + creator: dummyActionCreator, + handler: ({ state, action }) => { + return { ...state, ...action.payload }; + }, + }); + + expect(() => { + faultyReducer.addHandler({ + creator: dummyActionCreator, + handler: ({ state }) => { + return state; + }, + }); + }).toThrow(); + }); + }); + }); +}); diff --git a/public/app/core/redux/reducerFactory.ts b/public/app/core/redux/reducerFactory.ts new file mode 100644 index 00000000000..0ce9ac95edd --- /dev/null +++ b/public/app/core/redux/reducerFactory.ts @@ -0,0 +1,55 @@ +import { GrafanaAction, GrafanaActionCreator } from './actionCreatorFactory'; +import { Reducer } from 'redux'; + +export interface ActionHandler { + state: State; + action: GrafanaAction; +} + +export interface ActionHandlerConfig { + creator: GrafanaActionCreator; + handler: (handler: ActionHandler) => State; +} + +export interface AddActionHandler { + addHandler: (config: ActionHandlerConfig) => CreateReducer; +} + +export interface CreateReducer extends AddActionHandler { + create: () => Reducer>; +} + +export const reducerFactory = (initialState: State): AddActionHandler => { + const allHandlerConfigs: Array> = []; + + const addHandler = (config: ActionHandlerConfig): CreateReducer => { + if (allHandlerConfigs.some(c => c.creator.type === config.creator.type)) { + throw new Error(`There is already a handlers defined with the type ${config.creator.type}`); + } + + allHandlerConfigs.push(config); + + return instance; + }; + + const create = (): Reducer> => { + const reducer: Reducer> = (state: State = initialState, action: GrafanaAction) => { + const validHandlers = allHandlerConfigs + .filter(config => config.creator.type === action.type) + .map(config => config.handler); + + return validHandlers.reduce((currentState, handler) => { + return handler({ state: currentState, action }); + }, state || initialState); + }; + + return reducer; + }; + + const instance: CreateReducer = { + addHandler, + create, + }; + + return instance; +};