From 09e5b9c922c59602c1b789144d80af964880f6c0 Mon Sep 17 00:00:00 2001 From: Kai Moseley Date: Tue, 8 Aug 2017 16:10:41 +0100 Subject: [PATCH] fix(object): retain deletions to properties/array elements --- package-lock.json | 2 +- src/__tests__/buildStoreChunk.test.js | 15 +++++++++++++-- src/__tests__/validatePayload.test.js | 15 ++++++++++++++- src/reducers/objectReducer.js | 19 +++++++++---------- src/validatePayload.js | 1 + 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6efbba..d848ada 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2002,7 +2002,7 @@ "dependencies": { "jest-cli": { "version": "20.0.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-20.0.4.tgz", + "resolved": "https://npm.onfido.co.uk:443/jest-cli/-/jest-cli-20.0.4.tgz", "integrity": "sha1-5TKxnYiuW8bEF+iwWTpv6VSx3JM=", "dev": true } diff --git a/src/__tests__/buildStoreChunk.test.js b/src/__tests__/buildStoreChunk.test.js index ce183b1..0dd0d0a 100644 --- a/src/__tests__/buildStoreChunk.test.js +++ b/src/__tests__/buildStoreChunk.test.js @@ -42,6 +42,9 @@ describe('buildStoreChunk', () => { innerNested3: Types.reducer(Types.string('baz')), }), }), + nested5: Types.reducer(Types.shape({ + arrayExample: Types.arrayOf(Types.string()), + })), }); const nonNestedChunk = buildStoreChunk('example2', Types.reducer(Types.string('foo'))); @@ -51,7 +54,7 @@ describe('buildStoreChunk', () => { })); it('Selectors object has the correct top level structure for a nested chunk', () => { - expect(Object.keys(chunk.selectors)).toEqual(['nested1', 'nested2', 'nested3', 'nested4']); + expect(Object.keys(chunk.selectors)).toEqual(['nested1', 'nested2', 'nested3', 'nested4', 'nested5']); }); it('Selectors object is a function for a non-nested chunk', () => { expect(isFunction(nonNestedChunk.selectors)).toBe(true); @@ -69,7 +72,7 @@ describe('buildStoreChunk', () => { describe('Actions', () => { it('Actions object has the correct top level structure for a nested chunk', () => { - expect(Object.keys(chunk.actions)).toEqual(['nested1', 'nested2', 'nested3', 'nested4']); + expect(Object.keys(chunk.actions)).toEqual(['nested1', 'nested2', 'nested3', 'nested4', 'nested5']); }); it('Actions object has the correct top level structure for a non nested chunk', () => { expect(Object.keys(nonNestedChunk.actions)).toEqual(['replace', 'reset']); @@ -97,6 +100,14 @@ describe('buildStoreChunk', () => { store.dispatch(chunk.actions.nested1.reset()); expect(chunk.selectors.nested1(store.getState())).toEqual('foo'); }); + + it('Dispatching an empty array property should replace existing array', () => { + store.dispatch(chunk.actions.nested5.replace({ arrayExample: ['2'] })); + store.dispatch(chunk.actions.nested5.update({ arrayExample: [] })); + expect(chunk.selectors.nested5(store.getState())).toEqual({ + arrayExample: [], + }); + }); }); describe('Combined actions and selectors (non nested chunk)', () => { diff --git a/src/__tests__/validatePayload.test.js b/src/__tests__/validatePayload.test.js index 51d498d..c001129 100644 --- a/src/__tests__/validatePayload.test.js +++ b/src/__tests__/validatePayload.test.js @@ -65,7 +65,10 @@ describe('Validation functionality', () => { }); it('Array should return an empty array if a non-array is passed', () => { expect(validateArray('foo')).toEqual([]); - }) + }); + it('Array should allow an empty array', () => { + expect(validateArray([])).toEqual([]); + }); }); describe('Objects', () => { @@ -138,6 +141,16 @@ describe('Validation functionality', () => { test2: 'bar', }); }); + + const testObjectStructure7 = Types.shape({ + test1: Types.arrayOf(Types.string()), + }); + it('Should allow an empty array to be passed for an array property', () => { + expect(validateShape(testObjectStructure7, { test1: [] })).toEqual({ + test1: [], + }); + }); + }); describe('Non covered types', () => { diff --git a/src/reducers/objectReducer.js b/src/reducers/objectReducer.js index b655624..b3ec672 100644 --- a/src/reducers/objectReducer.js +++ b/src/reducers/objectReducer.js @@ -40,7 +40,6 @@ export type ShapeReducerOptions = { //============================== import isObject from 'lodash/isObject'; import omit from 'lodash/omit'; -import merge from 'lodash/fp/merge'; import { validateShape } from '../validatePayload'; import { createReducerBehaviors } from '../reducers'; import { PROP_TYPES } from '../structure'; @@ -119,16 +118,16 @@ export function createReducer(objectStructure: StructureType, behaviors: ShapeRe if (matchedBehaviors.length) { //Sanitize the payload using the reducer shape, then apply the sanitized //payload to the state using the behavior linked to this action type. - return reduce((interimState, matchedBehavior) => merge( + return reduce((interimState, matchedBehavior) => ({ + ...interimState, + ...behaviors[matchedBehavior.type].reducer( interimState, - behaviors[matchedBehavior.type].reducer( - interimState, - behaviors[matchedBehavior.type].validate - ? validateShape(objectStructure, matchedBehavior.payload) - : matchedBehavior.payload, - initialState - ) - ), state)(matchedBehaviors); + behaviors[matchedBehavior.type].validate + ? validateShape(objectStructure, matchedBehavior.payload) + : matchedBehavior.payload, + initialState + ) + }), state)(matchedBehaviors); } return state; diff --git a/src/validatePayload.js b/src/validatePayload.js index 09eace6..72fe405 100644 --- a/src/validatePayload.js +++ b/src/validatePayload.js @@ -45,6 +45,7 @@ export function validateShape(objectStructure: any, value: mixed): Object { } const validatedValue = getTypeValidation(valueType().type)(valueType, value); + if (validatedValue === undefined) { console.warn(`The property, ${name}, was populated with a type ${ typeof value } which does not` + ` match that specified in the reducer configuration ${ wildcardKeyPresent ? ', nor did it match a wildcardKey': ''}. It has been stripped from` +