Improve test coverage to 100%
This commit is contained in:
parent
0f3ff8168b
commit
84361fdae9
|
@ -28,8 +28,11 @@ describe('buildStoreChunk', () => {
|
|||
describe('Resulting chunk', () => {
|
||||
const chunk = buildStoreChunk('example', {
|
||||
nested1: Types.reducer(Types.string('foo')),
|
||||
nested2: Types.reducer(Types.shape()),
|
||||
nested3: Types.reducer(Types.arrayOf(Types.number())),
|
||||
nested2: Types.reducer(Types.shape({
|
||||
foo: Types.number(),
|
||||
bar: Types.string(),
|
||||
})),
|
||||
nested3: Types.reducer(Types.arrayOf(Types.number(), [1, 2, 3])),
|
||||
nested4: Types.reducer({
|
||||
innerNested1: Types.reducer(Types.string('bar')),
|
||||
innerNested2: Types.reducer({
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import {
|
||||
DEFAULT_ARRAY_BEHAVIORS,
|
||||
applyValidation,
|
||||
createReducer,
|
||||
} from '../arrayReducer';
|
||||
import {
|
||||
createReducerBehaviors,
|
||||
} from '../../reducers';
|
||||
import {
|
||||
Types
|
||||
} from '../../structure';
|
||||
|
||||
describe('arrayReducer', () => {
|
||||
|
||||
describe('behaviors', () => {
|
||||
|
||||
describe('replaceAtIndex', () => {
|
||||
const { replaceAtIndex } = DEFAULT_ARRAY_BEHAVIORS;
|
||||
it('should update at index correctly', () => {
|
||||
expect(replaceAtIndex.reducer([1,2,3], 4, [], 0)).toEqual([4,2,3]);
|
||||
});
|
||||
it('should return state if index not passed', () => {
|
||||
expect(replaceAtIndex.reducer([1,2,3], 4, [])).toEqual([1,2,3]);
|
||||
});
|
||||
it('should return state if no payload passed', () => {
|
||||
expect(replaceAtIndex.reducer([1,2,3], undefined, [], 0)).toEqual([1,2,3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetAtIndex', () => {
|
||||
const { resetAtIndex } = DEFAULT_ARRAY_BEHAVIORS;
|
||||
it('should reset at index correctly', () => {
|
||||
expect(resetAtIndex.reducer([1,2,3], undefined, 0, 0)).toEqual([0,2,3]);
|
||||
});
|
||||
it('should return state if no index provided', () => {
|
||||
expect(resetAtIndex.reducer([1,2,3], undefined, 0)).toEqual([1,2,3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeAtIndex', () => {
|
||||
const { removeAtIndex } = DEFAULT_ARRAY_BEHAVIORS;
|
||||
it('should remove at index correctly', () => {
|
||||
expect(removeAtIndex.reducer([1,2,3], undefined, undefined, 0)).toEqual([2,3]);
|
||||
});
|
||||
it('should return state if no index provided', () => {
|
||||
expect(removeAtIndex.reducer([1,2,3])).toEqual([1,2,3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replace', () => {
|
||||
const { replace } = DEFAULT_ARRAY_BEHAVIORS;
|
||||
it('should return state if payload is not an array', () => {
|
||||
expect(replace.reducer([1,2,3], '')).toEqual([1,2,3]);
|
||||
});
|
||||
it('should return the new array', () => {
|
||||
expect(replace.reducer([1,2,3], [4,5,6])).toEqual([4,5,6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
const { reset } = DEFAULT_ARRAY_BEHAVIORS;
|
||||
it('should reset the state', () => {
|
||||
expect(reset.reducer([1,2,3], undefined, [4,5,6])).toEqual([4,5,6]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('applyValidation', () => {
|
||||
const arrayStructure = Types.arrayOf(Types.number());
|
||||
const arrayStructure2 = Types.arrayOf(Types.shape({ foo: Types.string() }));
|
||||
it('should validate arrays correctly', () => {
|
||||
expect(applyValidation(arrayStructure, [1,2,3])).toEqual([1,2,3]);
|
||||
expect(applyValidation(arrayStructure, [1, 'foo', 3])).toEqual([1, 3]);
|
||||
});
|
||||
it('should validate non array primitive payloads correctly', () => {
|
||||
expect(applyValidation(arrayStructure, 1)).toEqual(1);
|
||||
});
|
||||
it('should validate none array object payloads correctly', () => {
|
||||
expect(applyValidation(arrayStructure2, { foo: 'toast' })).toEqual({ foo: 'toast' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('createReducer', () => {
|
||||
const arrayStructure = Types.arrayOf(Types.number());
|
||||
const reducer = createReducer(arrayStructure, createReducerBehaviors(DEFAULT_ARRAY_BEHAVIORS, 'string'));
|
||||
|
||||
it('should call the correct behavior', () => {
|
||||
expect(reducer([1,2,3], {
|
||||
type: 'string.replaceAtIndex',
|
||||
payload: 4,
|
||||
index: 0,
|
||||
})).toEqual([4,2,3]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
import {
|
||||
DEFAULT_SHAPE_BEHAVIORS,
|
||||
calculateDefaults,
|
||||
createReducer,
|
||||
} from '../objectReducer';
|
||||
import {
|
||||
createReducerBehaviors,
|
||||
} from '../../reducers';
|
||||
import { Types } from '../../structure';
|
||||
|
||||
describe('ObjectReducer', () => {
|
||||
|
||||
describe('behaviors', () => {
|
||||
|
||||
describe('replace', () => {
|
||||
const { replace } = DEFAULT_SHAPE_BEHAVIORS;
|
||||
it('reducer should return the state if the payload is undefined', () => {
|
||||
expect(replace.reducer({ foo: 1 }, undefined)).toEqual({ foo: 1 });
|
||||
});
|
||||
it('reducer should return the new state', () => {
|
||||
expect(replace.reducer({ foo: 1 }, { foo: 2 })).toEqual({ foo: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
const { reset } = DEFAULT_SHAPE_BEHAVIORS;
|
||||
it('reducer should return the initial state', () => {
|
||||
expect(reset.reducer({ foo: 1 }, undefined, { foo: 2 })).toEqual({ foo: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
const { update } = DEFAULT_SHAPE_BEHAVIORS;
|
||||
it('reducer should return the shallow merged new state', () => {
|
||||
expect(update.reducer({ foo: 1, bar: 2 }, { foo: 3 })).toEqual({ foo: 3, bar: 2 });
|
||||
});
|
||||
it('reducer should return state if payload is not an object', () => {
|
||||
expect(update.reducer({ foo: 1, bar: 2}, 'toast')).toEqual({ foo: 1, bar: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('calculateDefaults', () => {
|
||||
const structure = {
|
||||
foo: Types.string('foo'),
|
||||
bar: Types.shape({
|
||||
baz: Types.number(5),
|
||||
}),
|
||||
};
|
||||
|
||||
expect(calculateDefaults(structure)).toEqual({
|
||||
foo: 'foo',
|
||||
bar: {
|
||||
baz: 5,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('createReducer', () => {
|
||||
const shapeStructure = Types.shape({
|
||||
foo: Types.string('foo'),
|
||||
bar: Types.shape({
|
||||
baz: Types.number(5),
|
||||
}),
|
||||
});
|
||||
const reducer = createReducer(shapeStructure, createReducerBehaviors(DEFAULT_SHAPE_BEHAVIORS, 'string'));
|
||||
it('call the correct behavior', () => {
|
||||
expect(reducer({ foo: 'foo', bar: { baz: 5 }}, {
|
||||
type: 'string.update',
|
||||
payload: { foo: 'toast'}
|
||||
})).toEqual({ foo: 'toast', bar: { baz: 5 }});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import { DEFAULT_PRIMITIVE_BEHAVIORS } from '../primitiveReducer';
|
||||
|
||||
describe('PrimitiveReducer', () => {
|
||||
|
||||
describe('behaviors', () => {
|
||||
|
||||
describe('replace', () => {
|
||||
const { replace } = DEFAULT_PRIMITIVE_BEHAVIORS;
|
||||
it('reducer should return the state if the payload is undefined', () => {
|
||||
expect(replace.reducer('foo', undefined)).toEqual('foo');
|
||||
});
|
||||
it('reducer should return the new state', () => {
|
||||
expect(replace.reducer('foo', 'bar')).toEqual('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
const { reset } = DEFAULT_PRIMITIVE_BEHAVIORS;
|
||||
it('reducer should return the initial state', () => {
|
||||
expect(reset.reducer('foo', undefined, 'bar')).toEqual('bar');
|
||||
});
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -10,10 +10,10 @@ import type { ArrayStructureType } from '../structure';
|
|||
export type ArrayReducerAction = {
|
||||
type: string,
|
||||
payload: any,
|
||||
index: number,
|
||||
index?: number,
|
||||
};
|
||||
export type ArrayReducer = (state: Array<any>, action: ArrayReducerAction) => Array<any>;
|
||||
export type ArrayReducerBehavior = (state: Array<any>, payload: any, initialState: Array<any>, index: number | void) => Array<any>;
|
||||
export type ArrayReducerBehavior = (state: Array<any>, payload: any, initialState: Array<any>, index: number) => Array<any>;
|
||||
export type ArrayReducerBehaviorsConfig = {
|
||||
[key: string]: {
|
||||
action?: (value: any) => any,
|
||||
|
@ -62,42 +62,31 @@ function checkIndex(index: ?number, payload: any, behaviorName: string): boolean
|
|||
// make the end user replace the correct index themselves. However, it made sense
|
||||
// to create a few helper behaviors to aid with the most common array operations.
|
||||
//==============================
|
||||
const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = {
|
||||
export const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = {
|
||||
//Index specific behaviors.
|
||||
updateAtIndex: {
|
||||
reducer(state, payload, initialState, index = -1) {
|
||||
replaceAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
if (!checkIndex(index, payload, 'updateAtIndex')) return state;
|
||||
if (payload === undefined) return console.warn('Undefined was passed when updating index. Update not performed') || state;
|
||||
if (isArray(payload) || isObject(payload)) return updateAtIndex(state, { ...state[index], ...payload }, index);
|
||||
return updateAtIndex(state, payload, index);
|
||||
}
|
||||
},
|
||||
resetAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
checkIndex(index, payload, 'resetAtIndex');
|
||||
if (!checkIndex(index, payload, 'resetAtIndex')) return state;
|
||||
return updateAtIndex(state, initialState, index);
|
||||
}
|
||||
},
|
||||
removeAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
checkIndex(index, payload, 'removeAtIndex');
|
||||
if (!checkIndex(index, payload, 'removeAtIndex')) return state;
|
||||
return removeAtIndex(state, index);
|
||||
}
|
||||
},
|
||||
replaceAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
checkIndex(index, payload, 'replaceAtIndex');
|
||||
if (payload === undefined) console.warn('Undefined was passed when updating index. Update not performed');
|
||||
return updateAtIndex(state, payload, index);
|
||||
}
|
||||
},
|
||||
//Whole array behaviors.
|
||||
replace: {
|
||||
action(value) {
|
||||
if(!isArray(value)) throw new Error('An array must be provided when replacing an array');
|
||||
return value;
|
||||
},
|
||||
reducer(state, payload) {
|
||||
if(!isArray(payload)) return console.warn('An array must be provided when replacing an array') || state;
|
||||
return payload;
|
||||
}
|
||||
},
|
||||
|
@ -112,7 +101,7 @@ const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = {
|
|||
export function createArrayReducer(arrayTypeDescription: ArrayStructureType, {
|
||||
locationString,
|
||||
name,
|
||||
}: ArrayReducerOptions = {}) {
|
||||
}: ArrayReducerOptions) {
|
||||
return {
|
||||
reducers: {
|
||||
[name]: createReducer(arrayTypeDescription, createReducerBehaviors(DEFAULT_ARRAY_BEHAVIORS, locationString))
|
||||
|
@ -122,7 +111,7 @@ export function createArrayReducer(arrayTypeDescription: ArrayStructureType, {
|
|||
}
|
||||
|
||||
|
||||
function createReducer(arrayTypeDescription: ArrayStructureType, behaviors: ArrayReducerBehaviors): ArrayReducer {
|
||||
export function createReducer(arrayTypeDescription: ArrayStructureType, behaviors: ArrayReducerBehaviors): ArrayReducer {
|
||||
//Take the initial value specified as the default for the array, then apply it, using the validation
|
||||
//when doing so. The initial value must be an array.
|
||||
const initialValue = validateArray(arrayTypeDescription, arrayTypeDescription().defaultValue);
|
||||
|
@ -139,7 +128,7 @@ function createReducer(arrayTypeDescription: ArrayStructureType, behaviors: Arra
|
|||
}
|
||||
|
||||
|
||||
function applyValidation(arrayTypeDescription: ArrayStructureType, payload: any) {
|
||||
export function applyValidation(arrayTypeDescription: ArrayStructureType, payload: any) {
|
||||
// Array validation is more tricky than object/primitive, as it is possible that the current
|
||||
// action may involve updating the contents of a specific array element, rather than the
|
||||
// whole array. As a result, some extra functionality is required to determine which
|
||||
|
@ -161,9 +150,9 @@ function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationStr
|
|||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce(behaviorsConfig, (memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (value: Array<any>, index: ?number) => ({
|
||||
[name]: (payload: Array<any>, index: ?number) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (() => value))(value),
|
||||
payload: (behavior.action || (payload => payload))(payload),
|
||||
index,
|
||||
})
|
||||
}), {});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//==============================
|
||||
// Flow imports
|
||||
//==============================
|
||||
import type { StructureType } from '../structure';
|
||||
import type { StructureType, ShapeStructure } from '../structure';
|
||||
|
||||
|
||||
//==============================
|
||||
|
@ -37,7 +37,7 @@ export type ShapeReducerOptions = {
|
|||
//==============================
|
||||
// JS imports
|
||||
//==============================
|
||||
import { reduce } from 'lodash';
|
||||
import { reduce, isObject } from 'lodash';
|
||||
import { validateShape } from '../validatePayload';
|
||||
import { createReducerBehaviors } from '../reducers';
|
||||
import { PROP_TYPES } from '../structure';
|
||||
|
@ -50,10 +50,10 @@ import { PROP_TYPES } from '../structure';
|
|||
// payload and the previous state in a shallow way. This supplements the replace
|
||||
// behavior, which still replaces the previous state with the payload.
|
||||
//==============================
|
||||
const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = {
|
||||
export const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = {
|
||||
update: {
|
||||
action(value) { return value },
|
||||
reducer(state, payload = {}) {
|
||||
reducer(state, payload) {
|
||||
if (!isObject(payload)) return state;
|
||||
return { ...state, ...payload };
|
||||
}
|
||||
},
|
||||
|
@ -63,8 +63,8 @@ const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = {
|
|||
}
|
||||
},
|
||||
replace: {
|
||||
action(value) { return value },
|
||||
reducer(state, payload = {}) {
|
||||
reducer(state, payload) {
|
||||
if (!payload) return state;
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
@ -74,17 +74,17 @@ const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = {
|
|||
export function createShapeReducer(reducerShape: StructureType, {
|
||||
locationString,
|
||||
name,
|
||||
}: ShapeReducerOptions = {}) {
|
||||
}: ShapeReducerOptions) {
|
||||
return {
|
||||
reducers: {
|
||||
[name]: createReducer(reducerShape, createReducerBehaviors(DEFAULT_SHAPE_BEHAVIORS, locationString)),
|
||||
},
|
||||
actions: createActions(DEFAULT_SHAPE_BEHAVIORS, locationString, {}),
|
||||
actions: createActions(DEFAULT_SHAPE_BEHAVIORS, locationString),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function calculateDefaults(reducerStructure) {
|
||||
export function calculateDefaults(reducerStructure: any) {
|
||||
return reduce(reducerStructure, (memo, propValue, propName) => ({
|
||||
...memo,
|
||||
[propName]: propValue().type === PROP_TYPES._shape
|
||||
|
@ -94,7 +94,7 @@ function calculateDefaults(reducerStructure) {
|
|||
}
|
||||
|
||||
|
||||
function createReducer(objectStructure: StructureType, behaviors: ShapeReducerBehaviors): ShapeReducer {
|
||||
export function createReducer(objectStructure: StructureType, behaviors: ShapeReducerBehaviors): ShapeReducer {
|
||||
const initialState: Object = validateShape(objectStructure, calculateDefaults(objectStructure().structure));
|
||||
return (state = initialState, { type, payload }: ShapeReducerAction) => {
|
||||
//If the action type does not match any of the specified behaviors, just return the current state.
|
||||
|
@ -107,13 +107,13 @@ function createReducer(objectStructure: StructureType, behaviors: ShapeReducerBe
|
|||
}
|
||||
|
||||
|
||||
function createActions(behaviorsConfig: ShapeReducerBehaviorsConfig, locationString: string, defaultPayload: any): ShapeActions {
|
||||
function createActions(behaviorsConfig: ShapeReducerBehaviorsConfig, locationString: string): ShapeActions {
|
||||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce(behaviorsConfig, (memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (value: Object) => ({
|
||||
[name]: (payload: Object) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (() => defaultPayload))(value),
|
||||
payload: (behavior.action || (payload => payload))(payload),
|
||||
})
|
||||
}), {});
|
||||
}
|
||||
|
|
|
@ -47,9 +47,8 @@ import { createReducerBehaviors } from '../reducers';
|
|||
// reset behaviors by default.
|
||||
//==============================
|
||||
|
||||
const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = {
|
||||
export const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = {
|
||||
replace: {
|
||||
action(value) { return value },
|
||||
reducer(state, payload) {
|
||||
if (payload === undefined) return state;
|
||||
return payload;
|
||||
|
@ -66,12 +65,12 @@ const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = {
|
|||
export function createPrimitiveReducer(primitiveType: PrimitiveType, {
|
||||
locationString,
|
||||
name,
|
||||
}: PrimitiveReducerOptions = {}) {
|
||||
}: PrimitiveReducerOptions) {
|
||||
return {
|
||||
reducers: {
|
||||
[name]: createReducer(primitiveType, createReducerBehaviors(DEFAULT_PRIMITIVE_BEHAVIORS, locationString)),
|
||||
},
|
||||
actions: createActions(DEFAULT_PRIMITIVE_BEHAVIORS, locationString, {}),
|
||||
actions: createActions(DEFAULT_PRIMITIVE_BEHAVIORS, locationString),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -89,13 +88,13 @@ function createReducer(primitiveType: PrimitiveType, behaviors: PrimitiveReducer
|
|||
}
|
||||
|
||||
|
||||
function createActions(behaviorsConfig: PrimitiveReducerBehaviorsConfig, locationString: string, defaultPayload: any): PrimitiveActions {
|
||||
function createActions(behaviorsConfig: PrimitiveReducerBehaviorsConfig, locationString: string): PrimitiveActions {
|
||||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce(behaviorsConfig, (memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (value: mixed) => ({
|
||||
[name]: (payload: mixed) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (() => defaultPayload))(value),
|
||||
payload: (behavior.action || (payload => payload))(payload),
|
||||
})
|
||||
}), {});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue