Merge pull request #15158 from grafana/hugoh/redux-poc

WIP: Reducing boilerplate code for Redux
This commit is contained in:
Torkel Ödegaard
2019-02-01 09:41:40 +01:00
committed by GitHub
12 changed files with 653 additions and 157 deletions

View File

@@ -0,0 +1,56 @@
import { reducerFactory, actionCreatorFactory } from 'app/core/redux';
import { reducerTester } from './reducerTester';
interface DummyState {
data: string[];
}
const initialState: DummyState = {
data: [],
};
const dummyAction = actionCreatorFactory<string>('dummyAction').create();
const mutatingReducer = reducerFactory(initialState)
.addMapper({
filter: dummyAction,
mapper: (state, action) => {
state.data.push(action.payload);
return state;
},
})
.create();
const okReducer = reducerFactory(initialState)
.addMapper({
filter: dummyAction,
mapper: (state, action) => {
return {
...state,
data: state.data.concat(action.payload),
};
},
})
.create();
describe('reducerTester', () => {
describe('when reducer mutates state', () => {
it('then it should throw', () => {
expect(() => {
reducerTester()
.givenReducer(mutatingReducer, initialState)
.whenActionIsDispatched(dummyAction('some string'));
}).toThrow();
});
});
describe('when reducer does not mutate state', () => {
it('then it should not throw', () => {
expect(() => {
reducerTester()
.givenReducer(okReducer, initialState)
.whenActionIsDispatched(dummyAction('some string'));
}).not.toThrow();
});
});
});

View File

@@ -0,0 +1,79 @@
import { Reducer } from 'redux';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
export interface Given<State> {
givenReducer: (reducer: Reducer<State, ActionOf<any>>, state: State) => When<State>;
}
export interface When<State> {
whenActionIsDispatched: (action: ActionOf<any>) => Then<State>;
}
export interface Then<State> {
thenStateShouldEqual: (state: State) => Then<State>;
}
interface ObjectType extends Object {
[key: string]: any;
}
const deepFreeze = <T>(obj: T): T => {
Object.freeze(obj);
const isNotException = (object: any, propertyName: any) =>
typeof object === 'function'
? propertyName !== 'caller' && propertyName !== 'callee' && propertyName !== 'arguments'
: true;
const hasOwnProp = Object.prototype.hasOwnProperty;
if (obj && obj instanceof Object) {
const object: ObjectType = obj;
Object.getOwnPropertyNames(object).forEach(propertyName => {
const objectProperty: any = object[propertyName];
if (
hasOwnProp.call(object, propertyName) &&
isNotException(object, propertyName) &&
objectProperty &&
(typeof objectProperty === 'object' || typeof objectProperty === 'function') &&
Object.isFrozen(objectProperty) === false
) {
deepFreeze(objectProperty);
}
});
}
return obj;
};
interface ReducerTester<State> extends Given<State>, When<State>, Then<State> {}
export const reducerTester = <State>(): Given<State> => {
let reducerUnderTest: Reducer<State, ActionOf<any>> = null;
let resultingState: State = null;
let initialState: State = null;
const givenReducer = (reducer: Reducer<State, ActionOf<any>>, state: State): When<State> => {
reducerUnderTest = reducer;
initialState = { ...state };
initialState = deepFreeze(initialState);
return instance;
};
const whenActionIsDispatched = (action: ActionOf<any>): Then<State> => {
resultingState = reducerUnderTest(initialState, action);
return instance;
};
const thenStateShouldEqual = (state: State): Then<State> => {
expect(state).toEqual(resultingState);
return instance;
};
const instance: ReducerTester<State> = { thenStateShouldEqual, givenReducer, whenActionIsDispatched };
return instance;
};