Parcourir la source

improved testing of redux-gen

Tomi Cvetic il y a 8 ans
Parent
commit
b2f415203b

+ 7 - 9
package.json

@@ -3,6 +3,7 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "dotenv": "4.0.0",
     "lodash": "4.17.4",
     "mongoose": "4.9.0",
     "react": "15.4.2",
@@ -13,6 +14,7 @@
     "react-router-redux": "4.0.8",
     "redux": "3.6.0",
     "redux-saga": "0.14.3",
+    "request-promise": "4.2.0",
     "reselect": "2.5.4",
     "style-loader": "0.13.2",
     "stylus-loader": "3.0.1",
@@ -20,17 +22,13 @@
   },
   "devDependencies": {
     "autoprefixer-stylus": "0.13.0",
-    "babel-cli": "^6.24.0",
-    "babel-plugin-syntax-object-rest-spread": "^6.13.0",
-    "babel-plugin-transform-es2015-spread": "^6.22.0",
+    "babel-cli": "6.24.0",
     "babel-plugin-transform-object-rest-spread": "^6.23.0",
-    "babel-preset-es2015": "^6.24.0",
-    "babel-preset-es2016": "^6.22.0",
-    "babel-preset-latest": "^6.24.0",
-    "body-parser": "^1.17.1",
+    "babel-preset-latest": "6.24.0",
+    "body-parser": "1.17.1",
     "concurrently": "3.4.0",
-    "jsdoc": "^3.4.3",
-    "node-babel": "^0.1.2",
+    "jsdoc": "3.4.3",
+    "node-babel": "0.1.2",
     "react-scripts": "0.9.5",
     "stylus": "0.54.5"
   },

+ 21 - 0
src/__mocks__/localStorage.js

@@ -0,0 +1,21 @@
+class LocalStorageMock {
+  constructor () {
+    this.store = {}
+    this.outOfMemory = false
+  }
+  clear () {
+    this.store = {}
+  }
+  getItem (key) {
+    return this.store[key]
+  }
+  setItem (key, value) {
+    if (!this.outOfMemory) {
+      this.store[key] = value
+    } else {
+      throw new Error('Out of memory!')
+    }
+  }
+}
+
+global.localStorage = new LocalStorageMock()

+ 12 - 70
src/project/state.js

@@ -8,87 +8,29 @@
  * - actions
  **/
 
-import genStuff from 'helpers'
+import combineReducers from 'redux'
+import reduxCrudGen from 'redux-gen/crud'
+import reduxBasicGen from 'redux-gen/basic'
 import { NAME, DATA } from './constants'
 // import { call, put, takeEvery } from 'redux-saga/effects'
 
-const herbie = genStuff('project', ['create', 'update', 'remove'], false, api = null)
-/** actionTypes define what actions are handeled by the reducer. */
-const actionTypes = {}
-export const actionCreators = {}
-
-// Generate default actionsTypes CREATE, UPDATE, REMOVE
-DATA.forEach((dataItem, idx) => {
-  ['create_request', 'create_success', 'create_fail',
-    'update_request', 'update_success', 'update_fail',
-    'remove_request', 'remove_success', 'remove_fail'].forEach(action => {
-    // The Redux convention is to name action types e.g. demo_module/UPDATE
-    // For action creators, we define here the name e.g. removePrimary(id, data)
-    // where id is the element id and data is the element itself.
-      const actionType = `${action.toUpperCase()}_${dataItem.toUpperCase()}`
-      const actionName = `${action}${dataItem[0].toUpperCase()}${dataItem.substring(1)}`
-      if (idx === 0) {
-        actionCreators[actionName] = (id, data) => { return { type: `${NAME}/${actionType}`, id, data } }
-      } else {
-        actionCreators[actionName] = (primaryId, id, data) => { return { type: `${NAME}/${actionType}`, primaryId, id, data } }
-      }
-      actionTypes[actionType] = `${NAME}/${actionType}`
-    })
-})
+const basic = reduxBasicGen()
+const crud = reduxCrudGen()
 
-// Add specific action creators here:
-// actionCreators['loadSamples'] = () => { return { type: `${NAME}/LOAD_SAMPLES` } }
-// actionTypes['LOAD_SAMPLES'] = `${NAME}/LOAD_SAMPLES`
+/** actionTypes define what actions are handeled by the reducer. */
+const actions = { ...basic.actions, ...crud.actions }
+console.log('State actions', actions)
 
 /** state definition */
 /** It is generally easier to not have another object here. */
-export const state = []
+export const state = [ ...basic.state, ...crud.state ]
 console.log('State state', state)
 
 /** reducer is called by the redux dispatcher and handles all component actions */
-export function reducer (state = {}, action) {
-  switch (action.type) {
-    case actionTypes.CREATE_REQUEST:
-
-    case actionTypes.CREATE:
-      return {
-        ...state,
-        [action.project.projectId]: action.project
-      }
-    case actionTypes.UPDATE:
-      return {
-        ...state,
-        [action.projectId]: action.project
-      }
-    case actionTypes.REMOVE:
-      return {
-        ...state,
-        [action.projectId]: null
-      }
-    default:
-      return state
-  }
-}
+export const reducer = combineReducers(basic.reducer, crud.reducer)
 
 /** sagas are asynchronous workers (JS generators) to handle the state. */
-// Worker
-export function * incrementAsync () {
-  try {
-    const data = yield call(Api.isIncrementOk)
-    yield put({ type: 'INCREMENT_SUCCESS', data })
-  } catch (error) {
-    yield put({ type: 'INCREMENT_FAIL', error})
-  }
-}
+export const workers = crud.workers
 
 // Watcher (intercepts INCREMENT_REQUEST, dispatches INCREMENT_SUCCESS or INCREMENT_FAIL in return.)
-export function * watchIncrementAsync () {
-  yield takeEvery('INCREMENT_REQUEST', incrementAsync)
-}
-
-// export all sagas in parallel
-function * sagas () {
-  yield takeEvery('INCREMENT_REQUEST')
-  yield takeEvery('DECREMENT_REQUEST')
-}
-export const sagas = null
+export const watchers = crud.watchers

+ 1 - 0
src/redux-gen/crud.js

@@ -52,6 +52,7 @@ export function crudLocalstorageApi (action, options) {
         try {
           const items = localStorage.getItem(name) || {}
           items[action.id] = action.data
+          console.log(items)
           localStorage.setItem(name, items)
           resolve(null)
         } catch (error) {

+ 30 - 2
src/redux-gen/crud.test.js

@@ -3,7 +3,6 @@
   */
 
 import { crudHttpApi, crudLocalstorageApi, reduxCrudGen } from './crud'
-import { put, call, takeEvery } from 'redux-saga/effects'
 
 describe('HTTP API', () => {
   it('opens http connections', () => {
@@ -19,13 +18,42 @@ describe('HTTP API', () => {
   })
 })
 
+import '../__mocks__/localStorage'
 describe('LocalStorage API', () => {
+  let action
+  const options = {}
+  it('writes to localstorage', () => {
+    expect(localStorage.store.project).toBeUndefined()
+    action = { type: 'project/CREATE_REQUEST', data: { key: 'value' } }
+    return crudLocalstorageApi(action, options).then(data => {
+      expect(data).toBe(null)
+      expect(localStorage.store.project).toEqual(action.data)
+    })
+  })
+  it('updates localstorage', () => {
+    action = { type: 'project/UPDATE_REQUEST', data: { key: 'new value' } }
+    return crudLocalstorageApi(action, options).then(data => {
+      expect(data).toBe(null)
+      expect(localStorage.store.project).toEqual(action.data)
+    })
+  })
   it('reads from localstorage', () => {
+    action = { type: 'project/READ_REQUEST' }
+    return crudLocalstorageApi(action, options).then(data => {
+      expect(data).toEqual({ key: 'new value' })
+    })
   })
-  it('writes to localstorage', () => {
+  it('deletes from localstorage', () => {
+    action = { type: 'project/DELETE_REQUEST' }
+    return crudLocalstorageApi(action, options).then(data => {
+      expect(data).toBe(null)
+      expect(localStorage.store.project).toEqual({})
+    })
   })
 })
 
+import { put, call, takeEvery } from 'redux-saga/effects'
+
 describe('Crud Generator', () => {
   const api = () => null
   const project = reduxCrudGen('project', 'PROJ', api)

+ 0 - 84
src/redux-gen/localstorage.js

@@ -1,84 +0,0 @@
-import { call, put, takeEvery } from 'redux-sagas/effects'
-
-/** genStuff
-  * you input the name of a module (e.g. project) and the API (e.g. http://localhost/db/project)
-  * it outputs a state, action types, action creators, reducer, workers and watchers
-  * to handle a restful API.
-  */
-export function reduxLocalstorageGen (name) {
-  const actionTypes = {}
-  const actions = {}
-
-  /** Supported actions */
-  const actionList = ['save', 'restore']
-
-  /** For each supported action create an action creator to request
-    * data from the API, a success action when data is processed, and
-    * a failure action, when the API call was not successful.
-    * You will get e.g. create_request, update_success... */
-  actionList.forEach(action => {
-    /** generate sth like 'createProject' which dispatches 'project/CREATE_REQUEST' */
-    const actionName = `${action}${name[0].toUpperCase()}${name.substring(1)}`
-    actions[actionName] = (id, data) => { return { type: `${name}/${action}_REQUEST`, id, data } }
-    ['request', 'success', 'failure'].forEach(mode => {
-      /** generate sth like 'project/CREATE_REQUEST' */
-      const actionType = `${action}_${mode}`.toUpperCase()
-      actionTypes[actionType] = `${name}/${actionType}`
-    })
-  })
-  console.log('RESTGen: actionTypes, actions', actionTypes, actions)
-
-  const state = []
-  console.log('RESTGen: state', state)
-
-  function * worker (action) {
-    try {
-      const data = yield call(api, payload)
-      yield put({ type: '??_SUCCESS', data })
-    } catch (error) {
-      yield put({ type: '??_FAILURE', error })
-    }
-  }
-
-  function * watcher () {
-    for (action in actionList) {
-      yield takeEvery(`${name}/${action.toUpperCase()}_REQUEST`, worker)
-    }
-  }
-
-  function reducer (state = [], action) {
-    let nextState
-    switch (action.type) {
-      // On success,
-      case actionTypes[`CREATE_REQUEST_${name.toUpperCase()}`]:
-        worker(action)
-        return nextState
-      case actionTypes[`CREATE_SUCCESS_${name.toUpperCase()}`]:
-        nextState = [ ...state, action.data ]
-        return nextState
-      case actionTypes[`UPDATE_REQUEST_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
-        return nextState
-      case actionTypes[`UPDATE_SUCCESS_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
-        return nextState
-      case actionTypes[`REMOVE_REQUEST_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
-        return nextState
-      case actionTypes[`REMOVE_SUCCESS_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
-        return nextState
-      default:
-        return state
-    }
-  }
-
-  return {
-    actionTypes,
-    actions,
-    state,
-    reducer,
-    worker: (sync) ? null : worker,
-    watcher: (sync) ? null : watcher
-  }
-}