Improve test coverage to 100%

This commit is contained in:
Kai Moseley 2016-12-11 17:24:01 +00:00
parent 0f3ff8168b
commit 84361fdae9
7 changed files with 237 additions and 47 deletions

View File

@ -28,8 +28,11 @@ describe('buildStoreChunk', () => {
describe('Resulting chunk', () => { describe('Resulting chunk', () => {
const chunk = buildStoreChunk('example', { const chunk = buildStoreChunk('example', {
nested1: Types.reducer(Types.string('foo')), nested1: Types.reducer(Types.string('foo')),
nested2: Types.reducer(Types.shape()), nested2: Types.reducer(Types.shape({
nested3: Types.reducer(Types.arrayOf(Types.number())), foo: Types.number(),
bar: Types.string(),
})),
nested3: Types.reducer(Types.arrayOf(Types.number(), [1, 2, 3])),
nested4: Types.reducer({ nested4: Types.reducer({
innerNested1: Types.reducer(Types.string('bar')), innerNested1: Types.reducer(Types.string('bar')),
innerNested2: Types.reducer({ innerNested2: Types.reducer({

View File

@ -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]);
});
});
});

View File

@ -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 }});
});
});
});

View File

@ -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');
});
})
});
});

View File

@ -10,10 +10,10 @@ import type { ArrayStructureType } from '../structure';
export type ArrayReducerAction = { export type ArrayReducerAction = {
type: string, type: string,
payload: any, payload: any,
index: number, index?: number,
}; };
export type ArrayReducer = (state: Array<any>, action: ArrayReducerAction) => Array<any>; 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 = { export type ArrayReducerBehaviorsConfig = {
[key: string]: { [key: string]: {
action?: (value: any) => any, 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 // 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. // 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. //Index specific behaviors.
updateAtIndex: { replaceAtIndex: {
reducer(state, payload, initialState, index = -1) { reducer(state, payload, initialState, index) {
if (!checkIndex(index, payload, 'updateAtIndex')) return state; if (!checkIndex(index, payload, 'updateAtIndex')) return state;
if (payload === undefined) return console.warn('Undefined was passed when updating index. Update not performed') || 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); return updateAtIndex(state, payload, index);
} }
}, },
resetAtIndex: { resetAtIndex: {
reducer(state, payload, initialState, index) { reducer(state, payload, initialState, index) {
checkIndex(index, payload, 'resetAtIndex'); if (!checkIndex(index, payload, 'resetAtIndex')) return state;
return updateAtIndex(state, initialState, index); return updateAtIndex(state, initialState, index);
} }
}, },
removeAtIndex: { removeAtIndex: {
reducer(state, payload, initialState, index) { reducer(state, payload, initialState, index) {
checkIndex(index, payload, 'removeAtIndex'); if (!checkIndex(index, payload, 'removeAtIndex')) return state;
return removeAtIndex(state, index); 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. //Whole array behaviors.
replace: { replace: {
action(value) {
if(!isArray(value)) throw new Error('An array must be provided when replacing an array');
return value;
},
reducer(state, payload) { reducer(state, payload) {
if(!isArray(payload)) return console.warn('An array must be provided when replacing an array') || state;
return payload; return payload;
} }
}, },
@ -112,7 +101,7 @@ const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = {
export function createArrayReducer(arrayTypeDescription: ArrayStructureType, { export function createArrayReducer(arrayTypeDescription: ArrayStructureType, {
locationString, locationString,
name, name,
}: ArrayReducerOptions = {}) { }: ArrayReducerOptions) {
return { return {
reducers: { reducers: {
[name]: createReducer(arrayTypeDescription, createReducerBehaviors(DEFAULT_ARRAY_BEHAVIORS, locationString)) [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 //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. //when doing so. The initial value must be an array.
const initialValue = validateArray(arrayTypeDescription, arrayTypeDescription().defaultValue); 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 // 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 // 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 // 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 //Take a reducer behavior config object, and create actions using the location string
return reduce(behaviorsConfig, (memo, behavior, name) => ({ return reduce(behaviorsConfig, (memo, behavior, name) => ({
...memo, ...memo,
[name]: (value: Array<any>, index: ?number) => ({ [name]: (payload: Array<any>, index: ?number) => ({
type: `${locationString}.${name}`, type: `${locationString}.${name}`,
payload: (behavior.action || (() => value))(value), payload: (behavior.action || (payload => payload))(payload),
index, index,
}) })
}), {}); }), {});

View File

@ -2,7 +2,7 @@
//============================== //==============================
// Flow imports // Flow imports
//============================== //==============================
import type { StructureType } from '../structure'; import type { StructureType, ShapeStructure } from '../structure';
//============================== //==============================
@ -37,7 +37,7 @@ export type ShapeReducerOptions = {
//============================== //==============================
// JS imports // JS imports
//============================== //==============================
import { reduce } from 'lodash'; import { reduce, isObject } from 'lodash';
import { validateShape } from '../validatePayload'; import { validateShape } from '../validatePayload';
import { createReducerBehaviors } from '../reducers'; import { createReducerBehaviors } from '../reducers';
import { PROP_TYPES } from '../structure'; 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 // payload and the previous state in a shallow way. This supplements the replace
// behavior, which still replaces the previous state with the payload. // behavior, which still replaces the previous state with the payload.
//============================== //==============================
const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = { export const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = {
update: { update: {
action(value) { return value }, reducer(state, payload) {
reducer(state, payload = {}) { if (!isObject(payload)) return state;
return { ...state, ...payload }; return { ...state, ...payload };
} }
}, },
@ -63,8 +63,8 @@ const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = {
} }
}, },
replace: { replace: {
action(value) { return value }, reducer(state, payload) {
reducer(state, payload = {}) { if (!payload) return state;
return payload; return payload;
} }
} }
@ -74,17 +74,17 @@ const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = {
export function createShapeReducer(reducerShape: StructureType, { export function createShapeReducer(reducerShape: StructureType, {
locationString, locationString,
name, name,
}: ShapeReducerOptions = {}) { }: ShapeReducerOptions) {
return { return {
reducers: { reducers: {
[name]: createReducer(reducerShape, createReducerBehaviors(DEFAULT_SHAPE_BEHAVIORS, locationString)), [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) => ({ return reduce(reducerStructure, (memo, propValue, propName) => ({
...memo, ...memo,
[propName]: propValue().type === PROP_TYPES._shape [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)); const initialState: Object = validateShape(objectStructure, calculateDefaults(objectStructure().structure));
return (state = initialState, { type, payload }: ShapeReducerAction) => { return (state = initialState, { type, payload }: ShapeReducerAction) => {
//If the action type does not match any of the specified behaviors, just return the current state. //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 //Take a reducer behavior config object, and create actions using the location string
return reduce(behaviorsConfig, (memo, behavior, name) => ({ return reduce(behaviorsConfig, (memo, behavior, name) => ({
...memo, ...memo,
[name]: (value: Object) => ({ [name]: (payload: Object) => ({
type: `${locationString}.${name}`, type: `${locationString}.${name}`,
payload: (behavior.action || (() => defaultPayload))(value), payload: (behavior.action || (payload => payload))(payload),
}) })
}), {}); }), {});
} }

View File

@ -47,9 +47,8 @@ import { createReducerBehaviors } from '../reducers';
// reset behaviors by default. // reset behaviors by default.
//============================== //==============================
const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = { export const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = {
replace: { replace: {
action(value) { return value },
reducer(state, payload) { reducer(state, payload) {
if (payload === undefined) return state; if (payload === undefined) return state;
return payload; return payload;
@ -66,12 +65,12 @@ const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = {
export function createPrimitiveReducer(primitiveType: PrimitiveType, { export function createPrimitiveReducer(primitiveType: PrimitiveType, {
locationString, locationString,
name, name,
}: PrimitiveReducerOptions = {}) { }: PrimitiveReducerOptions) {
return { return {
reducers: { reducers: {
[name]: createReducer(primitiveType, createReducerBehaviors(DEFAULT_PRIMITIVE_BEHAVIORS, locationString)), [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 //Take a reducer behavior config object, and create actions using the location string
return reduce(behaviorsConfig, (memo, behavior, name) => ({ return reduce(behaviorsConfig, (memo, behavior, name) => ({
...memo, ...memo,
[name]: (value: mixed) => ({ [name]: (payload: mixed) => ({
type: `${locationString}.${name}`, type: `${locationString}.${name}`,
payload: (behavior.action || (() => defaultPayload))(value), payload: (behavior.action || (payload => payload))(payload),
}) })
}), {}); }), {});
} }