From 146cd18ef60be83a700fb06d1998b5667706e6c8 Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Tue, 5 Feb 2019 21:22:17 -0500 Subject: [PATCH 01/15] load profile state from storage --- app/state/actions/persistence.js | 10 +++++ .../persistence/__tests__/persistence.test.js | 38 +++++++++++++++++++ .../middlewares/persistence/persistence.js | 11 ++++++ .../profile/__tests__/code-request.test.js | 33 ++++++++++++++++ .../profile/__tests__/verification.test.js | 30 +++++++++++++++ app/state/reducers/profile/code-request.js | 17 +++++++++ app/state/reducers/profile/verification.js | 17 +++++++++ 7 files changed, 156 insertions(+) create mode 100644 app/state/actions/persistence.js create mode 100644 app/state/middlewares/persistence/__tests__/persistence.test.js create mode 100644 app/state/middlewares/persistence/persistence.js diff --git a/app/state/actions/persistence.js b/app/state/actions/persistence.js new file mode 100644 index 0000000..7f8e43c --- /dev/null +++ b/app/state/actions/persistence.js @@ -0,0 +1,10 @@ +export const STORAGE_LOAD_STATE = 'STORAGE:LOAD_STATE' + +export const loadState = (state) => { + return { + type: STORAGE_LOAD_STATE, + payload: { + state + } + } +} diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js new file mode 100644 index 0000000..48c2cd9 --- /dev/null +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -0,0 +1,38 @@ +import persistenceMiddleware from '@state/middlewares/persistence/persistence' + +describe('persistence middleware', () => { + let store, next, callPersistenceMiddleware, persistenceApi, stateFromStorage + beforeEach(() => { + stateFromStorage = { + someMockData: 'someMockData' + } + persistenceApi = { + getState: jest.fn(() => stateFromStorage) + } + store = { + dispatch: jest.fn(), + getState: jest.fn() + } + next = jest.fn() + callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) + }) + + describe('app init', () => { + it('should dispatch `STORAGE:LOAD_STATE` with payload from storage', () => { + const action = { + type: 'INIT', + payload: {} + } + + callPersistenceMiddleware(action) + + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'STORAGE:LOAD_STATE', + payload: { + state: stateFromStorage + } + }) + expect(store.dispatch).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/app/state/middlewares/persistence/persistence.js b/app/state/middlewares/persistence/persistence.js new file mode 100644 index 0000000..39dade3 --- /dev/null +++ b/app/state/middlewares/persistence/persistence.js @@ -0,0 +1,11 @@ +import { loadState } from '@state/actions/persistence' +import { INIT } from '@state/actions/app' + +const persistence = persistenceApi => store => next => action => { + if (action.type === INIT) { + const nextState = persistenceApi.getState() + store.dispatch(loadState(nextState)) + } +} + +export default persistence diff --git a/app/state/reducers/profile/__tests__/code-request.test.js b/app/state/reducers/profile/__tests__/code-request.test.js index fbfad75..2fe179f 100644 --- a/app/state/reducers/profile/__tests__/code-request.test.js +++ b/app/state/reducers/profile/__tests__/code-request.test.js @@ -109,4 +109,37 @@ describe('`profile.codeRequest` reducer', () => { }) }) }) + + describe('on action `STORAGE:LOAD_STATE`', () => { + it('should copy profile code-request state from storage', () => { + const state = { + isLoaded: false, + isLoading: false, + isSuccess: false + } + const stateFromStorage = { + profile: { + codeRequest: { + isLoaded: 'isLoaded', + isLoading: 'isLoading', + isSuccess: 'isSuccess' + } + } + } + const action = { + type: 'STORAGE:LOAD_STATE', + payload: { + state: stateFromStorage + } + } + + const nextState = profileCodeRequestReducer(state, action) + + expect(nextState).toEqual({ + isLoaded: 'isLoaded', + isLoading: 'isLoading', + isSuccess: 'isSuccess' + }) + }) + }) }) \ No newline at end of file diff --git a/app/state/reducers/profile/__tests__/verification.test.js b/app/state/reducers/profile/__tests__/verification.test.js index c1d8da9..50123af 100644 --- a/app/state/reducers/profile/__tests__/verification.test.js +++ b/app/state/reducers/profile/__tests__/verification.test.js @@ -99,4 +99,34 @@ describe('`profile.verification` reducer', () => { }) }) }) + + describe('on action `STORAGE:LOAD_STATE`', () => { + it('should copy profile verification state from storage', () => { + const state = { + isLoading: false, + isVerified: false + } + const stateFromStorage = { + profile: { + verification: { + isLoading: 'isLoading', + isVerified: 'isVerified' + } + } + } + const action = { + type: 'STORAGE:LOAD_STATE', + payload: { + state: stateFromStorage + } + } + + const nextState = profileVerificationReducer(state, action) + + expect(nextState).toEqual({ + isLoading: 'isLoading', + isVerified: 'isVerified' + }) + }) + }) }) diff --git a/app/state/reducers/profile/code-request.js b/app/state/reducers/profile/code-request.js index 7280891..e889909 100644 --- a/app/state/reducers/profile/code-request.js +++ b/app/state/reducers/profile/code-request.js @@ -3,6 +3,7 @@ import { VERIFICATION_REQUEST_CODE_SUCCESS, VERIFICATION_REQUEST_CODE_FAILURE } from '@state/actions/profile' +import { STORAGE_LOAD_STATE } from '@state/actions/persistence'; const initialState = { isLoading: false, @@ -35,6 +36,22 @@ const codeRequest = (state = initialState, action) => { } } + case STORAGE_LOAD_STATE: { + const { + payload: { + state: { + profile: { + codeRequest + } + } + } + } = action + + return { + ...codeRequest + } + } + default: return state } diff --git a/app/state/reducers/profile/verification.js b/app/state/reducers/profile/verification.js index 7976e27..f7317f0 100644 --- a/app/state/reducers/profile/verification.js +++ b/app/state/reducers/profile/verification.js @@ -3,6 +3,7 @@ import { VERIFICATION_SUBMIT_CODE_SUCCESS, VERIFICATION_SUBMIT_CODE_FAILURE } from '@state/actions/profile' +import { STORAGE_LOAD_STATE } from '@state/actions/persistence' const initialState = { isLoading: false, @@ -32,6 +33,22 @@ const verification = (state = initialState, action) => { } } + case STORAGE_LOAD_STATE: { + const { + payload: { + state: { + profile: { + verification + } + } + } + } = action + + return { + ...verification + } + } + default: return state } From fee3971e874ba5f27bba2522446588e826696928 Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Tue, 5 Feb 2019 21:45:27 -0500 Subject: [PATCH 02/15] persistence middleware handles undefined state from storage --- .../persistence/__tests__/persistence.test.js | 12 ++++++++++++ app/state/middlewares/persistence/persistence.js | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js index 48c2cd9..3753998 100644 --- a/app/state/middlewares/persistence/__tests__/persistence.test.js +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -34,5 +34,17 @@ describe('persistence middleware', () => { }) expect(store.dispatch).toHaveBeenCalledTimes(1) }) + + it('should not dispatch any action if state from storage is `undefined`', () => { + persistenceApi.getState = jest.fn(() => undefined) + const action = { + type: 'INIT', + payload: {} + } + + callPersistenceMiddleware(action) + + expect(store.dispatch).not.toHaveBeenCalled() + }) }) }) diff --git a/app/state/middlewares/persistence/persistence.js b/app/state/middlewares/persistence/persistence.js index 39dade3..e7714d5 100644 --- a/app/state/middlewares/persistence/persistence.js +++ b/app/state/middlewares/persistence/persistence.js @@ -4,7 +4,9 @@ import { INIT } from '@state/actions/app' const persistence = persistenceApi => store => next => action => { if (action.type === INIT) { const nextState = persistenceApi.getState() - store.dispatch(loadState(nextState)) + if (nextState != undefined) { + store.dispatch(loadState(nextState)) + } } } From 60ebefe8e33a26efe4bb9552dfe225c107f5df72 Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Tue, 5 Feb 2019 22:39:12 -0500 Subject: [PATCH 03/15] persistence api loads state from storage --- .../__tests__/persistence-api.test.js | 59 +++++++++++++++++++ app/persistence-api/index.js | 3 + app/persistence-api/package.json | 3 + app/persistence-api/persistence-api.js | 14 +++++ 4 files changed, 79 insertions(+) create mode 100644 app/persistence-api/__tests__/persistence-api.test.js create mode 100644 app/persistence-api/index.js create mode 100644 app/persistence-api/package.json create mode 100644 app/persistence-api/persistence-api.js diff --git a/app/persistence-api/__tests__/persistence-api.test.js b/app/persistence-api/__tests__/persistence-api.test.js new file mode 100644 index 0000000..5475438 --- /dev/null +++ b/app/persistence-api/__tests__/persistence-api.test.js @@ -0,0 +1,59 @@ +// import PersistenceApi from '@persistence-api/persistence-api' + +describe('persistence-api', () => { + describe('getState', () => { + describe('state exists in storage', () => { + let PersistenceApi + beforeEach(() => { + jest.resetAllMocks() + jest.mock('react-native', () => { + return { + AsyncStorage: { + getItem: jest.fn(() => { + return new Promise(resolve => + resolve({ + mockedData: 'mockedData' + }) + ) + }) + } + } + }) + PersistenceApi = require('@persistence-api/persistence-api').default + }) + + it('should return state from storage', async () => { + const state = await PersistenceApi.getState() + + expect(state).toEqual({ + mockedData: 'mockedData' + }) + }) + }) + + describe('state does not exist in storage', () => { + let PersistenceApi + beforeEach(() => { + jest.resetAllMocks() + jest.doMock('react-native', () => { + return { + AsyncStorage: { + getItem: jest.fn(() => { + return new Promise((_, reject) => + reject() + ) + }) + } + } + }) + PersistenceApi = require('@persistence-api/persistence-api').default + }) + + it('should return `undefined`', async () => { + const state = await PersistenceApi.getState() + + expect(state).toEqual(undefined) + }) + }) + }) +}) diff --git a/app/persistence-api/index.js b/app/persistence-api/index.js new file mode 100644 index 0000000..889ce43 --- /dev/null +++ b/app/persistence-api/index.js @@ -0,0 +1,3 @@ +import persistenceApi from './persistence-api' + +export default persistenceApi diff --git a/app/persistence-api/package.json b/app/persistence-api/package.json new file mode 100644 index 0000000..ce32576 --- /dev/null +++ b/app/persistence-api/package.json @@ -0,0 +1,3 @@ +{ + "name": "@persistence-api" +} diff --git a/app/persistence-api/persistence-api.js b/app/persistence-api/persistence-api.js new file mode 100644 index 0000000..43d602d --- /dev/null +++ b/app/persistence-api/persistence-api.js @@ -0,0 +1,14 @@ +import { AsyncStorage } from 'react-native' + +const STORAGE_KEY_STATE = 'STORAGE:STATE' + +export default class PersistenceApi { + static getState = async () => { + try { + return await AsyncStorage.getItem(STORAGE_KEY_STATE) + } + catch (error) { + return undefined + } + } +} \ No newline at end of file From 78a6835aa31cdc971dacaad0ddf1adb2b523270e Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Wed, 6 Feb 2019 22:21:16 -0500 Subject: [PATCH 04/15] use `persistence` middleware with `PersistenceApi` in store --- .../__tests__/persistence-api.test.js | 38 ++++++++----------- app/persistence-api/persistence-api.js | 5 ++- .../persistence/__tests__/persistence.test.js | 10 +++-- app/state/middlewares/persistence/index.js | 3 ++ .../middlewares/persistence/persistence.js | 6 +-- app/state/store/store.js | 3 ++ 6 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 app/state/middlewares/persistence/index.js diff --git a/app/persistence-api/__tests__/persistence-api.test.js b/app/persistence-api/__tests__/persistence-api.test.js index 5475438..4cb67ce 100644 --- a/app/persistence-api/__tests__/persistence-api.test.js +++ b/app/persistence-api/__tests__/persistence-api.test.js @@ -6,19 +6,15 @@ describe('persistence-api', () => { let PersistenceApi beforeEach(() => { jest.resetAllMocks() - jest.mock('react-native', () => { - return { - AsyncStorage: { - getItem: jest.fn(() => { - return new Promise(resolve => - resolve({ - mockedData: 'mockedData' - }) - ) - }) - } + jest.mock('react-native', () => ({ + AsyncStorage: { + getItem: jest.fn(() => + new Promise(resolve => + resolve('{"mockedData": "mockedData"}') + ) + ) } - }) + })) PersistenceApi = require('@persistence-api/persistence-api').default }) @@ -35,17 +31,15 @@ describe('persistence-api', () => { let PersistenceApi beforeEach(() => { jest.resetAllMocks() - jest.doMock('react-native', () => { - return { - AsyncStorage: { - getItem: jest.fn(() => { - return new Promise((_, reject) => - reject() - ) - }) - } + jest.doMock('react-native', () => ({ + AsyncStorage: { + getItem: jest.fn(() => { + return new Promise(resolve => + resolve('{}') + ) + }) } - }) + })) PersistenceApi = require('@persistence-api/persistence-api').default }) diff --git a/app/persistence-api/persistence-api.js b/app/persistence-api/persistence-api.js index 43d602d..c5b5623 100644 --- a/app/persistence-api/persistence-api.js +++ b/app/persistence-api/persistence-api.js @@ -4,10 +4,11 @@ const STORAGE_KEY_STATE = 'STORAGE:STATE' export default class PersistenceApi { static getState = async () => { + const result = await AsyncStorage.getItem(STORAGE_KEY_STATE) try { - return await AsyncStorage.getItem(STORAGE_KEY_STATE) + return JSON.parse(result) } - catch (error) { + catch (_) { return undefined } } diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js index 3753998..99d04a2 100644 --- a/app/state/middlewares/persistence/__tests__/persistence.test.js +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -7,7 +7,11 @@ describe('persistence middleware', () => { someMockData: 'someMockData' } persistenceApi = { - getState: jest.fn(() => stateFromStorage) + getState: jest.fn(() => + new Promise(resolve => + resolve(stateFromStorage) + ) + ) } store = { dispatch: jest.fn(), @@ -18,13 +22,13 @@ describe('persistence middleware', () => { }) describe('app init', () => { - it('should dispatch `STORAGE:LOAD_STATE` with payload from storage', () => { + it('should dispatch `STORAGE:LOAD_STATE` with payload from storage', async () => { const action = { type: 'INIT', payload: {} } - callPersistenceMiddleware(action) + await callPersistenceMiddleware(action) expect(store.dispatch).toHaveBeenCalledWith({ type: 'STORAGE:LOAD_STATE', diff --git a/app/state/middlewares/persistence/index.js b/app/state/middlewares/persistence/index.js new file mode 100644 index 0000000..19a928c --- /dev/null +++ b/app/state/middlewares/persistence/index.js @@ -0,0 +1,3 @@ +import persistenceMiddleware from './persistence' + +export default persistenceMiddleware diff --git a/app/state/middlewares/persistence/persistence.js b/app/state/middlewares/persistence/persistence.js index e7714d5..47d9b59 100644 --- a/app/state/middlewares/persistence/persistence.js +++ b/app/state/middlewares/persistence/persistence.js @@ -1,10 +1,10 @@ import { loadState } from '@state/actions/persistence' import { INIT } from '@state/actions/app' -const persistence = persistenceApi => store => next => action => { +const persistence = persistenceApi => store => next => async action => { if (action.type === INIT) { - const nextState = persistenceApi.getState() - if (nextState != undefined) { + const nextState = await persistenceApi.getState() + if (nextState !== undefined) { store.dispatch(loadState(nextState)) } } diff --git a/app/state/store/store.js b/app/state/store/store.js index 39ca232..50879d0 100644 --- a/app/state/store/store.js +++ b/app/state/store/store.js @@ -4,9 +4,12 @@ import { apiInit } from '@state/actions/api' import reducer from '@state/reducers' import api from '@state/middlewares/api' import navigation from '@state/middlewares/navigation' +import persistence from '@state/middlewares/persistence'; import getOrCreateWsClient from '@hooligram-api' +import PersistenceApi from '@persistence-api' const middlewares = [ + persistence(PersistenceApi), api(getOrCreateWsClient), navigation(NavigationActions), (store) => next => action => { From 8294d2db3e6181bd3423c18fb57f4739223e26fc Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Wed, 6 Feb 2019 22:23:01 -0500 Subject: [PATCH 05/15] dispatch `INIT` action on app startup --- app/index.js | 5 +++++ app/state/actions/app.js | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 app/state/actions/app.js diff --git a/app/index.js b/app/index.js index e0bc8fd..2d225c9 100644 --- a/app/index.js +++ b/app/index.js @@ -1,5 +1,6 @@ import React, { Component } from 'react' import { Provider } from 'react-redux' +import { init } from '@state/actions/app' import store from '@state/store' import OnboardingStackNavigation from '@navigation/onboarding-stack' import { setTopLevelNavigator } from '@state/middlewares/navigation/navigation' @@ -13,5 +14,9 @@ export default class App extends Component { ) } + componentDidMount() { + store.dispatch(init()) + } + goToNextScreen = () => console.log('goToNextScreen') } diff --git a/app/state/actions/app.js b/app/state/actions/app.js new file mode 100644 index 0000000..dead27c --- /dev/null +++ b/app/state/actions/app.js @@ -0,0 +1,8 @@ +export const INIT = 'INIT' + +export const init = () => { + return { + type: INIT, + payload: {} + } +} From eaa26a6508d77b7f573eefa1ad1d640d146d33e0 Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Wed, 6 Feb 2019 22:24:59 -0500 Subject: [PATCH 06/15] refactor: dispatch `API_INIT` at a better place --- app/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/index.js b/app/index.js index 2d225c9..ce25e43 100644 --- a/app/index.js +++ b/app/index.js @@ -1,6 +1,7 @@ import React, { Component } from 'react' import { Provider } from 'react-redux' import { init } from '@state/actions/app' +import { apiInit } from '@state/actions/api' import store from '@state/store' import OnboardingStackNavigation from '@navigation/onboarding-stack' import { setTopLevelNavigator } from '@state/middlewares/navigation/navigation' @@ -16,6 +17,7 @@ export default class App extends Component { componentDidMount() { store.dispatch(init()) + store.dispatch(apiInit()) } goToNextScreen = () => console.log('goToNextScreen') From c8c53adbeef1f3a592e8e43bc53b7bdce92f1f56 Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Wed, 6 Feb 2019 22:26:27 -0500 Subject: [PATCH 07/15] logger middleware --- app/state/middlewares/logger/index.js | 7 +++++++ app/state/store/store.js | 14 +++----------- 2 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 app/state/middlewares/logger/index.js diff --git a/app/state/middlewares/logger/index.js b/app/state/middlewares/logger/index.js new file mode 100644 index 0000000..f2be0f2 --- /dev/null +++ b/app/state/middlewares/logger/index.js @@ -0,0 +1,7 @@ +export default store => next => action => { + console.log('prevState', store.getState()) + console.log('action', action) + const nextAction = next(action) + console.log('nextState', store.getState()) + return nextAction +} diff --git a/app/state/store/store.js b/app/state/store/store.js index 50879d0..ce052db 100644 --- a/app/state/store/store.js +++ b/app/state/store/store.js @@ -1,10 +1,10 @@ import { createStore, applyMiddleware } from 'redux' import { NavigationActions } from 'react-navigation' -import { apiInit } from '@state/actions/api' import reducer from '@state/reducers' import api from '@state/middlewares/api' +import logger from '@state/middlewares/logger' import navigation from '@state/middlewares/navigation' -import persistence from '@state/middlewares/persistence'; +import persistence from '@state/middlewares/persistence' import getOrCreateWsClient from '@hooligram-api' import PersistenceApi from '@persistence-api' @@ -12,17 +12,9 @@ const middlewares = [ persistence(PersistenceApi), api(getOrCreateWsClient), navigation(NavigationActions), - (store) => next => action => { - console.log('prevState', store.getState()) - console.log('action', action) - const nextAction = next(action) - console.log('nextState', store.getState()) - return nextAction - }, + logger ] const store = createStore(reducer, applyMiddleware(...middlewares)) -store.dispatch(apiInit()) - export default store From 39e5ccdcdbd3266ffa9d92847554b9e24ca255dc Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Wed, 6 Feb 2019 22:31:04 -0500 Subject: [PATCH 08/15] refactor and fix persistence middleware tests --- .../persistence/__tests__/persistence.test.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js index 99d04a2..2b8c013 100644 --- a/app/state/middlewares/persistence/__tests__/persistence.test.js +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -22,12 +22,12 @@ describe('persistence middleware', () => { }) describe('app init', () => { - it('should dispatch `STORAGE:LOAD_STATE` with payload from storage', async () => { - const action = { - type: 'INIT', - payload: {} - } + const action = { + type: 'INIT', + payload: {} + } + it('should dispatch `STORAGE:LOAD_STATE` with payload from storage', async () => { await callPersistenceMiddleware(action) expect(store.dispatch).toHaveBeenCalledWith({ @@ -39,14 +39,10 @@ describe('persistence middleware', () => { expect(store.dispatch).toHaveBeenCalledTimes(1) }) - it('should not dispatch any action if state from storage is `undefined`', () => { + it('should not dispatch any action if state from storage is `undefined`', async () => { persistenceApi.getState = jest.fn(() => undefined) - const action = { - type: 'INIT', - payload: {} - } - callPersistenceMiddleware(action) + await callPersistenceMiddleware(action) expect(store.dispatch).not.toHaveBeenCalled() }) From 9de4d9e418a97c2b061c491d0a30f994c8c3a00f Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Wed, 6 Feb 2019 22:51:31 -0500 Subject: [PATCH 09/15] persistence middleware should propagate action to next middleware --- .../persistence/__tests__/persistence.test.js | 33 +++++++++++++++++-- .../middlewares/persistence/persistence.js | 2 ++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js index 2b8c013..c3b8924 100644 --- a/app/state/middlewares/persistence/__tests__/persistence.test.js +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -21,7 +21,7 @@ describe('persistence middleware', () => { callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) }) - describe('app init', () => { + describe('on app init action `INIT`', () => { const action = { type: 'INIT', payload: {} @@ -39,7 +39,7 @@ describe('persistence middleware', () => { expect(store.dispatch).toHaveBeenCalledTimes(1) }) - it('should not dispatch any action if state from storage is `undefined`', async () => { + it('should not dispatch any new action if state from storage is `undefined`', async () => { persistenceApi.getState = jest.fn(() => undefined) await callPersistenceMiddleware(action) @@ -47,4 +47,33 @@ describe('persistence middleware', () => { expect(store.dispatch).not.toHaveBeenCalled() }) }) + + describe('any action', () => { + const action = { + type: 'SOME_ACTION', + payload: { + somePayload: 'some payload' + } + } + + it('should call `next` properly', async () => { + await callPersistenceMiddleware(action) + + expect(next).toHaveBeenCalledWith(action) + expect(next).toHaveBeenCalledTimes(1) + }) + + it('should propagate returned action by next to store', async () => { + const expectedAction = { + type: 'SOME_ACTION_RETURNED_BY_NEXT', + payload: {} + } + next = jest.fn(() => expectedAction) + callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) + + const returnedAction = await callPersistenceMiddleware(action) + + expect(returnedAction).toEqual(expectedAction) + }) + }) }) diff --git a/app/state/middlewares/persistence/persistence.js b/app/state/middlewares/persistence/persistence.js index 47d9b59..fa73589 100644 --- a/app/state/middlewares/persistence/persistence.js +++ b/app/state/middlewares/persistence/persistence.js @@ -7,7 +7,9 @@ const persistence = persistenceApi => store => next => async action => { if (nextState !== undefined) { store.dispatch(loadState(nextState)) } + return next(action) } + return next(action) } export default persistence From ab4c3cadfc28938af889ecdf22797d12af228ea3 Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Wed, 6 Feb 2019 22:59:41 -0500 Subject: [PATCH 10/15] update storage with latest state on every action --- .../persistence/__tests__/persistence.test.js | 14 +++++++++++--- app/state/middlewares/persistence/persistence.js | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js index c3b8924..3fbed65 100644 --- a/app/state/middlewares/persistence/__tests__/persistence.test.js +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -1,7 +1,7 @@ import persistenceMiddleware from '@state/middlewares/persistence/persistence' describe('persistence middleware', () => { - let store, next, callPersistenceMiddleware, persistenceApi, stateFromStorage + let store, next, callPersistenceMiddleware, persistenceApi, stateFromStorage, stateFromStore beforeEach(() => { stateFromStorage = { someMockData: 'someMockData' @@ -11,11 +11,12 @@ describe('persistence middleware', () => { new Promise(resolve => resolve(stateFromStorage) ) - ) + ), + saveState: jest.fn() } store = { dispatch: jest.fn(), - getState: jest.fn() + getState: jest.fn(() => stateFromStore) } next = jest.fn() callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) @@ -75,5 +76,12 @@ describe('persistence middleware', () => { expect(returnedAction).toEqual(expectedAction) }) + + it('should update storage', async () => { + await callPersistenceMiddleware(action) + + expect(persistenceApi.saveState).toHaveBeenCalledWith(stateFromStore) + expect(persistenceApi.saveState).toHaveBeenCalledTimes(1) + }) }) }) diff --git a/app/state/middlewares/persistence/persistence.js b/app/state/middlewares/persistence/persistence.js index fa73589..43185c6 100644 --- a/app/state/middlewares/persistence/persistence.js +++ b/app/state/middlewares/persistence/persistence.js @@ -9,6 +9,10 @@ const persistence = persistenceApi => store => next => async action => { } return next(action) } + + const state = store.getState() + await persistenceApi.saveState(state) + return next(action) } From 948aa82fd60cce001cbe64393eac63cedaa8f5c8 Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Wed, 6 Feb 2019 23:31:59 -0500 Subject: [PATCH 11/15] persistence middleware handles `null` from storage --- .../middlewares/persistence/__tests__/persistence.test.js | 8 ++++++++ app/state/middlewares/persistence/persistence.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js index 3fbed65..1cecb85 100644 --- a/app/state/middlewares/persistence/__tests__/persistence.test.js +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -47,6 +47,14 @@ describe('persistence middleware', () => { expect(store.dispatch).not.toHaveBeenCalled() }) + + it('should not dispatch any new action if state from storage is `null`', async () => { + persistenceApi.getState = jest.fn(() => null) + + await callPersistenceMiddleware(action) + + expect(store.dispatch).not.toHaveBeenCalled() + }) }) describe('any action', () => { diff --git a/app/state/middlewares/persistence/persistence.js b/app/state/middlewares/persistence/persistence.js index 43185c6..7b17a72 100644 --- a/app/state/middlewares/persistence/persistence.js +++ b/app/state/middlewares/persistence/persistence.js @@ -4,7 +4,7 @@ import { INIT } from '@state/actions/app' const persistence = persistenceApi => store => next => async action => { if (action.type === INIT) { const nextState = await persistenceApi.getState() - if (nextState !== undefined) { + if (nextState !== undefined && nextState !== null) { store.dispatch(loadState(nextState)) } return next(action) From 7fbacdb6e523adde1100a123548621a703be011e Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Thu, 7 Feb 2019 00:44:38 -0500 Subject: [PATCH 12/15] PersistenceApi saves state --- .../__tests__/persistence-api.test.js | 78 ++++++++++++++++++- app/persistence-api/persistence-api.js | 7 +- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/app/persistence-api/__tests__/persistence-api.test.js b/app/persistence-api/__tests__/persistence-api.test.js index 4cb67ce..054f95a 100644 --- a/app/persistence-api/__tests__/persistence-api.test.js +++ b/app/persistence-api/__tests__/persistence-api.test.js @@ -12,7 +12,8 @@ describe('persistence-api', () => { new Promise(resolve => resolve('{"mockedData": "mockedData"}') ) - ) + ), + setItem: jest.fn() } })) PersistenceApi = require('@persistence-api/persistence-api').default @@ -37,7 +38,8 @@ describe('persistence-api', () => { return new Promise(resolve => resolve('{}') ) - }) + }), + setItem: jest.fn() } })) PersistenceApi = require('@persistence-api/persistence-api').default @@ -50,4 +52,76 @@ describe('persistence-api', () => { }) }) }) + + describe('saveState', () => { + let PersistenceApi, AsyncStorage, state + beforeEach(() => { + jest.resetAllMocks() + jest.mock('react-native', () => ({ + AsyncStorage: { + getItem: jest.fn(), + setItem: jest.fn((key, state) => + new Promise(resolve => + resolve(undefined) + ) + ) + } + })) + PersistenceApi = require('@persistence-api/persistence-api').default + AsyncStorage = require('react-native').AsyncStorage + }) + + describe('state is serializable', () => { + beforeEach(() => { + state = { + someState: 'some state' + } + }) + + it('should save state as string', async () => { + await PersistenceApi.saveState(state) + + expect(AsyncStorage.setItem).toHaveBeenCalledWith('STORAGE:STATE', '{\"someState\":\"some state\"}') + }) + }) + + describe('state is not serializable', () => { + let state + beforeEach(() => { + state = {}; + state.myself = state; + }) + + it('should throw error', async done => { + try { + await PersistenceApi.saveState(state) + fail() + } + catch (err) { + done() + } + }) + }) + + describe('Storage fails to save', () => { + let state + beforeEach(() => { + AsyncStorage.setItem = jest.fn(() => + new Promise((_, reject) => + reject() + ) + ) + }) + + it('should throw error', async done => { + try { + await PersistenceApi.saveState(state) + fail() + } + catch (err) { + done() + } + }) + }) + }) }) diff --git a/app/persistence-api/persistence-api.js b/app/persistence-api/persistence-api.js index c5b5623..2d92761 100644 --- a/app/persistence-api/persistence-api.js +++ b/app/persistence-api/persistence-api.js @@ -12,4 +12,9 @@ export default class PersistenceApi { return undefined } } -} \ No newline at end of file + + static saveState = async (state) => { + const stateString = JSON.stringify(state) + await AsyncStorage.setItem(STORAGE_KEY_STATE, stateString) + } +} From 3545def6250f59ff4bf76cb8ccd47c03d165c5cf Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Thu, 7 Feb 2019 00:46:16 -0500 Subject: [PATCH 13/15] persistence middleware save states and dispatches error action on persistence failure --- .../persistence/__tests__/persistence.test.js | 48 +++++++++++++++++++ .../middlewares/persistence/persistence.js | 28 +++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js index 1cecb85..57d51ab 100644 --- a/app/state/middlewares/persistence/__tests__/persistence.test.js +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -55,6 +55,30 @@ describe('persistence middleware', () => { expect(store.dispatch).not.toHaveBeenCalled() }) + + describe('PersistenceApi getState fails', () => { + let error + beforeEach(() => { + error = { someErrorObject: 'some error data' } + persistenceApi.getState = jest.fn(() => + new Promise((_, reject) => + reject(error) + ) + ) + callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) + }) + + it('should dispatch `STORAGE:LOAD_STATE_FAILURE` with error as payload', async () => { + await callPersistenceMiddleware(action) + + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'STORAGE:LOAD_STATE_FAILURE', + payload: { + error + } + }) + }) + }) }) describe('any action', () => { @@ -91,5 +115,29 @@ describe('persistence middleware', () => { expect(persistenceApi.saveState).toHaveBeenCalledWith(stateFromStore) expect(persistenceApi.saveState).toHaveBeenCalledTimes(1) }) + + describe('PersistenceApi saveState fails', () => { + let error + beforeEach(() => { + error = { someErrorObject: 'some error data' } + persistenceApi.saveState = jest.fn(() => + new Promise((_, reject) => + reject(error) + ) + ) + callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) + }) + + it('should dispatch `STORAGE:SAVE_STATE_FAILURE` with error as payload', async () => { + await callPersistenceMiddleware(action) + + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'STORAGE:SAVE_STATE_FAILURE', + payload: { + error + } + }) + }) + }) }) }) diff --git a/app/state/middlewares/persistence/persistence.js b/app/state/middlewares/persistence/persistence.js index 7b17a72..68df1a6 100644 --- a/app/state/middlewares/persistence/persistence.js +++ b/app/state/middlewares/persistence/persistence.js @@ -3,15 +3,37 @@ import { INIT } from '@state/actions/app' const persistence = persistenceApi => store => next => async action => { if (action.type === INIT) { - const nextState = await persistenceApi.getState() + let nextState + try { + nextState = await persistenceApi.getState() + } + catch (error) { + store.dispatch({ + type: 'STORAGE:LOAD_STATE_FAILURE', + payload: { + error + } + }) + } + if (nextState !== undefined && nextState !== null) { store.dispatch(loadState(nextState)) } return next(action) } - const state = store.getState() - await persistenceApi.saveState(state) + try { + const state = store.getState() + await persistenceApi.saveState(state) + } + catch (error) { + store.dispatch({ + type: 'STORAGE:SAVE_STATE_FAILURE', + payload: { + error + } + }) + } return next(action) } From ab0bc7866ce0d0408cd9f8d6c70a90e95adfe35d Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Thu, 7 Feb 2019 21:21:05 -0500 Subject: [PATCH 14/15] refactor: persistence actions as async --- app/state/actions/persistence.js | 53 +++++++++++- .../persistence/__tests__/persistence.test.js | 80 ++++++++++++------- .../middlewares/persistence/persistence.js | 49 +++++++----- .../profile/__tests__/code-request.test.js | 4 +- .../profile/__tests__/verification.test.js | 4 +- app/state/reducers/profile/code-request.js | 6 +- app/state/reducers/profile/verification.js | 4 +- 7 files changed, 142 insertions(+), 58 deletions(-) diff --git a/app/state/actions/persistence.js b/app/state/actions/persistence.js index 7f8e43c..2e9e04f 100644 --- a/app/state/actions/persistence.js +++ b/app/state/actions/persistence.js @@ -1,10 +1,57 @@ -export const STORAGE_LOAD_STATE = 'STORAGE:LOAD_STATE' +export const PERSISTENCE_LOAD_STATE_REQUEST = 'PERSISTENCE:LOAD_STATE_REQUEST' +export const PERSISTENCE_LOAD_STATE_SUCCESS = 'PERSISTENCE:LOAD_STATE_SUCCESS' +export const PERSISTENCE_LOAD_STATE_FAILURE = 'PERSISTENCE:LOAD_STATE_FAILURE' -export const loadState = (state) => { +export const PERSISTENCE_SAVE_STATE_REQUEST = 'PERSISTENCE:SAVE_STATE_REQUEST' +export const PERSISTENCE_SAVE_STATE_SUCCESS = 'PERSISTENCE:SAVE_STATE_SUCCESS' +export const PERSISTENCE_SAVE_STATE_FAILURE = 'PERSISTENCE:SAVE_STATE_FAILURE' + +export const loadStateRequest = () => { + return { + type: PERSISTENCE_LOAD_STATE_REQUEST, + payload: {} + } +} + +export const loadStateSuccess = (state) => { + return { + type: PERSISTENCE_LOAD_STATE_SUCCESS, + payload: { + state + } + } +} + +export const loadStateFailure = (error) => { + return { + type: PERSISTENCE_LOAD_STATE_FAILURE, + payload: { + error + } + } +} + +export const saveStateRequest = (state) => { return { - type: STORAGE_LOAD_STATE, + type: PERSISTENCE_SAVE_STATE_REQUEST, payload: { state } } } + +export const saveStateSuccess = () => { + return { + type: PERSISTENCE_SAVE_STATE_SUCCESS, + payload: {} + } +} + +export const saveStateFailure = (error) => { + return { + type: PERSISTENCE_SAVE_STATE_FAILURE, + payload: { + error + } + } +} diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js index 57d51ab..f1bc0e7 100644 --- a/app/state/middlewares/persistence/__tests__/persistence.test.js +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -28,35 +28,39 @@ describe('persistence middleware', () => { payload: {} } - it('should dispatch `STORAGE:LOAD_STATE` with payload from storage', async () => { + it('should try to request state from storage', async () => { await callPersistenceMiddleware(action) expect(store.dispatch).toHaveBeenCalledWith({ - type: 'STORAGE:LOAD_STATE', - payload: { - state: stateFromStorage - } + type: 'PERSISTENCE:LOAD_STATE_REQUEST', + payload: {} }) - expect(store.dispatch).toHaveBeenCalledTimes(1) - }) - - it('should not dispatch any new action if state from storage is `undefined`', async () => { - persistenceApi.getState = jest.fn(() => undefined) - - await callPersistenceMiddleware(action) - - expect(store.dispatch).not.toHaveBeenCalled() }) - it('should not dispatch any new action if state from storage is `null`', async () => { - persistenceApi.getState = jest.fn(() => null) - - await callPersistenceMiddleware(action) - - expect(store.dispatch).not.toHaveBeenCalled() + describe('storage returns null or undefined result', () => { + [undefined, null].forEach(result => { + beforeEach(() => { + persistenceApi.getState = jest.fn(() => + new Promise(resolve => + resolve(result) + ) + ) + }) + + it('should dispatch error with payload', async () => { + await callPersistenceMiddleware(action) + + expect(store.dispatch({ + type: 'PERSISTENCE:LOAD_STATE_FAILURE', + payload: { + error: new Error('Error: state in storage is `undefined` or `null`') + } + })) + }) + }) }) - describe('PersistenceApi getState fails', () => { + describe('storage fails to provide state', () => { let error beforeEach(() => { error = { someErrorObject: 'some error data' } @@ -68,11 +72,11 @@ describe('persistence middleware', () => { callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) }) - it('should dispatch `STORAGE:LOAD_STATE_FAILURE` with error as payload', async () => { + it('should dispatch error with payload', async () => { await callPersistenceMiddleware(action) expect(store.dispatch).toHaveBeenCalledWith({ - type: 'STORAGE:LOAD_STATE_FAILURE', + type: 'PERSISTENCE:LOAD_STATE_FAILURE', payload: { error } @@ -81,6 +85,26 @@ describe('persistence middleware', () => { }) }) + describe('action is a PERSISTENCE:* action', async () => { + const action = { + type: 'PERSISTENCE:SOME_ACTION', + payload: {} + } + expectedReturnedAction = { someAction: 'someAction' } + next = jest.fn(() => expectedReturnedAction) + + const returnedAction = await callPersistenceMiddleware(action) + + it('should not dispatch any action', () => { + expect(store.dispatch).not.toHaveBeenCalled() + }) + + it('should call propagate the action to next middleware', () => { + expect(store.next.toHaveBeenCalledWith(action)) + expect(returnedAction).toEqual(returnedAction) + }) + }) + describe('any action', () => { const action = { type: 'SOME_ACTION', @@ -96,7 +120,7 @@ describe('persistence middleware', () => { expect(next).toHaveBeenCalledTimes(1) }) - it('should propagate returned action by next to store', async () => { + it('should propagate action to next middleware', async () => { const expectedAction = { type: 'SOME_ACTION_RETURNED_BY_NEXT', payload: {} @@ -116,10 +140,10 @@ describe('persistence middleware', () => { expect(persistenceApi.saveState).toHaveBeenCalledTimes(1) }) - describe('PersistenceApi saveState fails', () => { + describe('storage fails to save state', () => { let error beforeEach(() => { - error = { someErrorObject: 'some error data' } + error = { someErrorData: 'some error data' } persistenceApi.saveState = jest.fn(() => new Promise((_, reject) => reject(error) @@ -128,11 +152,11 @@ describe('persistence middleware', () => { callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) }) - it('should dispatch `STORAGE:SAVE_STATE_FAILURE` with error as payload', async () => { + it('should dispatch error action with payload', async () => { await callPersistenceMiddleware(action) expect(store.dispatch).toHaveBeenCalledWith({ - type: 'STORAGE:SAVE_STATE_FAILURE', + type: 'PERSISTENCE:SAVE_STATE_FAILURE', payload: { error } diff --git a/app/state/middlewares/persistence/persistence.js b/app/state/middlewares/persistence/persistence.js index 68df1a6..ba4c96e 100644 --- a/app/state/middlewares/persistence/persistence.js +++ b/app/state/middlewares/persistence/persistence.js @@ -1,38 +1,51 @@ -import { loadState } from '@state/actions/persistence' +import { + loadStateRequest, + loadStateSuccess, + loadStateFailure, + saveStateRequest, + saveStateSuccess, + saveStateFailure +} from '@state/actions/persistence' import { INIT } from '@state/actions/app' const persistence = persistenceApi => store => next => async action => { + const { getState, dispatch } = store + if (action.type === INIT) { let nextState + try { + dispatch(loadStateRequest()) nextState = await persistenceApi.getState() + + if (nextState !== undefined && nextState !== null) { + dispatch(loadStateSuccess(nextState)) + } + else { + dispatch(loadStateFailure( + new Error('Error: state in storage is `undefined` or `null`') + )) + } } catch (error) { - store.dispatch({ - type: 'STORAGE:LOAD_STATE_FAILURE', - payload: { - error - } - }) + dispatch(loadStateFailure(error)) } - if (nextState !== undefined && nextState !== null) { - store.dispatch(loadState(nextState)) - } + return next(action) + } + + if (action.type.startsWith('PERSISTENCE:')) { return next(action) } try { - const state = store.getState() - await persistenceApi.saveState(state) + const nextState = getState() + dispatch(saveStateRequest(nextState)) + await persistenceApi.saveState(nextState) + dispatch(saveStateSuccess()) } catch (error) { - store.dispatch({ - type: 'STORAGE:SAVE_STATE_FAILURE', - payload: { - error - } - }) + dispatch(saveStateFailure(error)) } return next(action) diff --git a/app/state/reducers/profile/__tests__/code-request.test.js b/app/state/reducers/profile/__tests__/code-request.test.js index 2fe179f..0eb55b0 100644 --- a/app/state/reducers/profile/__tests__/code-request.test.js +++ b/app/state/reducers/profile/__tests__/code-request.test.js @@ -110,7 +110,7 @@ describe('`profile.codeRequest` reducer', () => { }) }) - describe('on action `STORAGE:LOAD_STATE`', () => { + describe('on action `PERSISTENCE:LOAD_STATE_SUCCESS`', () => { it('should copy profile code-request state from storage', () => { const state = { isLoaded: false, @@ -127,7 +127,7 @@ describe('`profile.codeRequest` reducer', () => { } } const action = { - type: 'STORAGE:LOAD_STATE', + type: 'PERSISTENCE:LOAD_STATE_SUCCESS', payload: { state: stateFromStorage } diff --git a/app/state/reducers/profile/__tests__/verification.test.js b/app/state/reducers/profile/__tests__/verification.test.js index 50123af..d857157 100644 --- a/app/state/reducers/profile/__tests__/verification.test.js +++ b/app/state/reducers/profile/__tests__/verification.test.js @@ -100,7 +100,7 @@ describe('`profile.verification` reducer', () => { }) }) - describe('on action `STORAGE:LOAD_STATE`', () => { + describe('on action `PERSISTENCE:LOAD_STATE_SUCCESS`', () => { it('should copy profile verification state from storage', () => { const state = { isLoading: false, @@ -115,7 +115,7 @@ describe('`profile.verification` reducer', () => { } } const action = { - type: 'STORAGE:LOAD_STATE', + type: 'PERSISTENCE:LOAD_STATE_SUCCESS', payload: { state: stateFromStorage } diff --git a/app/state/reducers/profile/code-request.js b/app/state/reducers/profile/code-request.js index e889909..13adeb9 100644 --- a/app/state/reducers/profile/code-request.js +++ b/app/state/reducers/profile/code-request.js @@ -3,7 +3,7 @@ import { VERIFICATION_REQUEST_CODE_SUCCESS, VERIFICATION_REQUEST_CODE_FAILURE } from '@state/actions/profile' -import { STORAGE_LOAD_STATE } from '@state/actions/persistence'; +import { PERSISTENCE_LOAD_STATE_SUCCESS } from '@state/actions/persistence'; const initialState = { isLoading: false, @@ -36,7 +36,7 @@ const codeRequest = (state = initialState, action) => { } } - case STORAGE_LOAD_STATE: { + case PERSISTENCE_LOAD_STATE_SUCCESS: { const { payload: { state: { @@ -46,7 +46,7 @@ const codeRequest = (state = initialState, action) => { } } } = action - + return { ...codeRequest } diff --git a/app/state/reducers/profile/verification.js b/app/state/reducers/profile/verification.js index f7317f0..b8d9964 100644 --- a/app/state/reducers/profile/verification.js +++ b/app/state/reducers/profile/verification.js @@ -3,7 +3,7 @@ import { VERIFICATION_SUBMIT_CODE_SUCCESS, VERIFICATION_SUBMIT_CODE_FAILURE } from '@state/actions/profile' -import { STORAGE_LOAD_STATE } from '@state/actions/persistence' +import { PERSISTENCE_LOAD_STATE_SUCCESS } from '@state/actions/persistence' const initialState = { isLoading: false, @@ -33,7 +33,7 @@ const verification = (state = initialState, action) => { } } - case STORAGE_LOAD_STATE: { + case PERSISTENCE_LOAD_STATE_SUCCESS: { const { payload: { state: { From 2338051c24a8f48d1740c4adf18edc201ee8c879 Mon Sep 17 00:00:00 2001 From: Imran Ariffin Date: Fri, 8 Feb 2019 06:19:16 -0500 Subject: [PATCH 15/15] refactor: persist state only on api response action --- .../persistence/__tests__/persistence.test.js | 156 ++++++++++-------- .../middlewares/persistence/persistence.js | 14 +- 2 files changed, 100 insertions(+), 70 deletions(-) diff --git a/app/state/middlewares/persistence/__tests__/persistence.test.js b/app/state/middlewares/persistence/__tests__/persistence.test.js index f1bc0e7..47868ef 100644 --- a/app/state/middlewares/persistence/__tests__/persistence.test.js +++ b/app/state/middlewares/persistence/__tests__/persistence.test.js @@ -85,81 +85,103 @@ describe('persistence middleware', () => { }) }) - describe('action is a PERSISTENCE:* action', async () => { - const action = { - type: 'PERSISTENCE:SOME_ACTION', - payload: {} - } - expectedReturnedAction = { someAction: 'someAction' } - next = jest.fn(() => expectedReturnedAction) - - const returnedAction = await callPersistenceMiddleware(action) - - it('should not dispatch any action', () => { - expect(store.dispatch).not.toHaveBeenCalled() - }) - - it('should call propagate the action to next middleware', () => { - expect(store.next.toHaveBeenCalledWith(action)) - expect(returnedAction).toEqual(returnedAction) - }) - }) - - describe('any action', () => { - const action = { - type: 'SOME_ACTION', - payload: { - somePayload: 'some payload' - } - } - - it('should call `next` properly', async () => { - await callPersistenceMiddleware(action) - - expect(next).toHaveBeenCalledWith(action) - expect(next).toHaveBeenCalledTimes(1) - }) - - it('should propagate action to next middleware', async () => { - const expectedAction = { - type: 'SOME_ACTION_RETURNED_BY_NEXT', + describe('action is a PERSISTENCE:* or not api response action', () => { + [ + 'PERSISTENCE:SOME_ACTION', + 'SOME_ACTION', + 'API:SOME_ACTION_REQUEST' + ] + .forEach(async actionType => { + const action = { + type: actionType, payload: {} } - next = jest.fn(() => expectedAction) - callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) - + expectedReturnedAction = { someAction: 'someAction' } + next = jest.fn(() => expectedReturnedAction) + const returnedAction = await callPersistenceMiddleware(action) - - expect(returnedAction).toEqual(expectedAction) - }) - - it('should update storage', async () => { - await callPersistenceMiddleware(action) - - expect(persistenceApi.saveState).toHaveBeenCalledWith(stateFromStore) - expect(persistenceApi.saveState).toHaveBeenCalledTimes(1) + + it('should not dispatch any action', () => { + expect(store.dispatch).not.toHaveBeenCalled() + }) + + it('should call propagate the action to next middleware', () => { + expect(store.next.toHaveBeenCalledWith(action)) + expect(returnedAction).toEqual(returnedAction) + }) }) + }) - describe('storage fails to save state', () => { - let error - beforeEach(() => { - error = { someErrorData: 'some error data' } - persistenceApi.saveState = jest.fn(() => - new Promise((_, reject) => - reject(error) - ) - ) + describe('action is an api response action', () => { + [ + 'API:SOME_ACTION_SUCCESS', + 'API:SOME_ACTION_FAILURE' + ] + .forEach(async actionType => { + const action = { + type: actionType, + payload: { + somePayload: 'some payload' + } + } + + it('should call `next` properly', async () => { + await callPersistenceMiddleware(action) + + expect(next).toHaveBeenCalledWith(action) + expect(next).toHaveBeenCalledTimes(1) + }) + + it('should propagate action to next middleware', async () => { + const expectedAction = { + type: 'SOME_ACTION_RETURNED_BY_NEXT', + payload: {} + } + next = jest.fn(() => expectedAction) callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) + + const returnedAction = await callPersistenceMiddleware(action) + + expect(returnedAction).toEqual(expectedAction) }) - - it('should dispatch error action with payload', async () => { + + it('should update storage with next state', async () => { + const prevState = 'prevState' + const nextState = 'nextState' + store.getState = jest + .fn() + .mockReturnValueOnce(() => prevState) + .mockImplementationOnce(() => nextState) + callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) + await callPersistenceMiddleware(action) - - expect(store.dispatch).toHaveBeenCalledWith({ - type: 'PERSISTENCE:SAVE_STATE_FAILURE', - payload: { - error - } + + expect(store.getState).toHaveBeenCalledTimes(2) + expect(persistenceApi.saveState).toHaveBeenCalledWith(nextState) + expect(persistenceApi.saveState).toHaveBeenCalledTimes(1) + }) + + describe('storage fails to save state', () => { + let error + beforeEach(() => { + error = { someErrorData: 'some error data' } + persistenceApi.saveState = jest.fn(() => + new Promise((_, reject) => + reject(error) + ) + ) + callPersistenceMiddleware = persistenceMiddleware(persistenceApi)(store)(next) + }) + + it('should dispatch error action with payload', async () => { + await callPersistenceMiddleware(action) + + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'PERSISTENCE:SAVE_STATE_FAILURE', + payload: { + error + } + }) }) }) }) diff --git a/app/state/middlewares/persistence/persistence.js b/app/state/middlewares/persistence/persistence.js index ba4c96e..f0acddd 100644 --- a/app/state/middlewares/persistence/persistence.js +++ b/app/state/middlewares/persistence/persistence.js @@ -34,12 +34,20 @@ const persistence = persistenceApi => store => next => async action => { return next(action) } - if (action.type.startsWith('PERSISTENCE:')) { + if ( + action.type.startsWith('PERSISTENCE:') && ( + !action.type.match(/API:(.*)_SUCCESS/) || + !action.type.match(/API:(.*)_FAILURE/) + ) + ) { return next(action) } + const prevState = getState() + const returnedAction = next(action) + const nextState = getState() + try { - const nextState = getState() dispatch(saveStateRequest(nextState)) await persistenceApi.saveState(nextState) dispatch(saveStateSuccess()) @@ -48,7 +56,7 @@ const persistence = persistenceApi => store => next => async action => { dispatch(saveStateFailure(error)) } - return next(action) + return returnedAction } export default persistence