├── .babelrc.json ├── .circleci └── config.yml ├── .codeclimate.yml ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── package.json ├── rollup.config.js ├── src ├── constants.js ├── createAction.js ├── createReducer.js ├── createThunk.js ├── index.js ├── statusReducer.js ├── thunkMiddleware.js ├── useLoading.js └── useStatus.js └── tests └── thunkMiddleware.spec.js /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@1.1.6 4 | jobs: 5 | build-and-test: 6 | executor: 7 | name: node/default 8 | steps: 9 | - checkout 10 | - node/with-cache: 11 | steps: 12 | - run: npm install 13 | - run: npm test 14 | workflows: 15 | build-and-test: 16 | jobs: 17 | - build-and-test 18 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | # Methods or functions defined with a high number of arguments 4 | argument-count: 5 | config: 6 | threshold: 5 7 | 8 | # Boolean logic that may be hard to understand 9 | complex-logic: 10 | config: 11 | threshold: 4 12 | 13 | # Excessive lines of code within a single file 14 | file-lines: 15 | config: 16 | threshold: 500 17 | 18 | # Functions or methods that may be hard to understand 19 | method-complexity: 20 | config: 21 | threshold: 20 22 | 23 | # Classes defined with a high number of functions or methods 24 | method-count: 25 | config: 26 | threshold: 20 27 | 28 | # Deeply nested control structures like if or case 29 | nested-control-flow: 30 | config: 31 | threshold: 4 32 | 33 | # Functions or methods with a high number of return statements 34 | return-statements: 35 | config: 36 | threshold: 4 37 | 38 | # Excessive lines of code within a single function or method 39 | # Disabled because the render function will exceed this threshold many times 40 | method-lines: 41 | enabled: false 42 | 43 | # Duplicate code which is not identical but shares the same structure (e.g. variable names may differ) 44 | # Disabled because it gives some false positives 45 | similar-code: 46 | enabled: false 47 | 48 | # Duplicate code which is syntactically identical (but may be formatted differently) 49 | # Disabled because it gives some false positives 50 | identical-code: 51 | enabled: false 52 | plugins: 53 | eslint: 54 | enabled: true 55 | config: 56 | config: ~/.eslintrc 57 | nodesecurity: 58 | enabled: true 59 | stylelint: 60 | enabled: true 61 | 62 | exclude_patterns: 63 | - "node_modules/" 64 | - "/test/**/*" 65 | - "**/vendor/" 66 | - "**/*.d.ts" 67 | - "coverage/**/*" 68 | - "src/locales/" 69 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /* 2 | !src 3 | !tests 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:import/errors", 12 | "plugin:import/warnings" 13 | ], 14 | "plugins": ["prettier"], 15 | "globals": { 16 | "Atomics": "readonly", 17 | "SharedArrayBuffer": "readonly" 18 | }, 19 | "parserOptions": { 20 | "ecmaVersion": 2018, 21 | "sourceType": "module" 22 | }, 23 | "rules": { 24 | "import/no-unresolved": ["error"], 25 | "indent": [ 26 | "error", 27 | 2 28 | ], 29 | "linebreak-style": [ 30 | "error", 31 | "unix" 32 | ], 33 | "quotes": [ 34 | "error", 35 | "single" 36 | ], 37 | "semi": [ 38 | "error", 39 | "never" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | lib 4 | 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at peter@rootstrap.com, ximena.lasserre@rootstrap.com, elizabeth@rootstrap.com, eugenia.miranda@rootstrap.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | MIT License 4 | 5 | Copyright (c) 2018 Rootstrap 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @rootstrap/redux-tools 2 | This package has some basic functionality common to both react bases. 3 | It includes a status reducer that lets you track the status of your async actions and a thunks-like middleware that will automatically dispatch success and failure actions. 4 | 5 | ## Basic usage 6 | 7 | ### ActionCreators 8 | 9 | This package provides an action creator utility, that together with the provided middleware will make it very easy to create side effects for your actions. 10 | This setup will automatically execute your side effect thunk and dispatch success or error actions when the thunk succeeds or fails, respectively. 11 | 12 | `createThunk` receives the action names prefix as the first argument and the async thunk as the second one. 13 | 14 | Example: 15 | ```js 16 | // src/actions/userActions.js 17 | import { createThunk } from '@rootstrap/redux-tools' 18 | 19 | export const getProfile = createThunk( 20 | 'GET_PROFILE', 21 | userService.getProfile, 22 | ); 23 | ``` 24 | 25 | You can then dispatch this `getProfile` action, and the middleware will automatically dispatch actions with types `GET_PROFILE_SUCCESS` or `GET_PROFILE_ERROR` for you. 26 | 27 | The returned object, (`getProfile` in the example above) has 4 properties you can use in order to handle the different dispatched actions in your reducer: 28 | - request 29 | - success 30 | - error 31 | - reset 32 | 33 | Following the previous example: 34 | 35 | ```js 36 | // src/reducers/userReducer.js 37 | 38 | import { getProfile } from 'src/actions/userActions'; 39 | 40 | const actionHandlers = { 41 | [getProfile.success]: (state, { payload }) => { 42 | state.user = payload; 43 | }, 44 | }; 45 | ``` 46 | 47 | If you need to access the store, or dispatch extra actions from your thunk, you can use `dispatch` and `getState` as the last two parameters. 48 | 49 | Example: 50 | 51 | Dispatching some custom analytics event that requires store data: 52 | ```js 53 | // src/actions/userActions.js 54 | import { createThunk } from '@rootstrap/redux-tools' 55 | 56 | export const getProfile = createThunk( 57 | 'GET_PROFILE', 58 | async (userId, dispatch, getState) => { 59 | const { analytics: { analyticsToken } } = getState(); 60 | const profile = await userService.getProfile(profileId); 61 | dispatch(analytics.logProfile(analyticsToken, profile)); 62 | return profile; 63 | }, 64 | ); 65 | ``` 66 | 67 | ### Status tracking 68 | 69 | To access status information on a component the `useStatus` hook is provided. 70 | The following status constants are exported: 71 | - LOADING 72 | - SUCCESS 73 | - ERROR 74 | 75 | Here is a simple example: 76 | 77 | ```js 78 | import { useStatus, useDispatch } from 'hooks'; 79 | import { getProfile } from 'src/actions/userActions'; 80 | import { SUCCESS, LOADING, ERROR } from '@rootstrap/redux-tools' 81 | 82 | const MyComponent = () => { 83 | const getProfileRequest = useDispatch(getProfile); 84 | const { status, error } = useStatus(getProfile); 85 | 86 | return <> 87 | 88 | {(status === LOADING) && } 89 | {(status === SUCCESS) && } 90 | {(status === ERROR) && } 91 | 92 | } 93 | ``` 94 | 95 | A `useLoading` hook is also available if you only care about loading status. It returns a boolean indicating whether the action is still loading or not. 96 | 97 | To reset the status of an action you can dispatch the `reset` action returned by `createThunk`. 98 | 99 | 100 | ## Installation guide 101 | 102 | ### Step 1: install the package 103 | 104 | `npm i @rootstrap/redux-tools` 105 | or 106 | `yarn add @rootstrap/redux-tools` 107 | 108 | ### Step 2: configure the reducer 109 | ```js 110 | // src/reducers/index.js 111 | import { combineReducers } from 'redux' 112 | import { statusReducer } from '@rootstrap/redux-tools' 113 | 114 | const rootReducer = combineReducers({ 115 | // ...your other reducers here 116 | // you have to pass statusReducer under 'statusReducer' key, 117 | statusReducer 118 | }) 119 | ``` 120 | 121 | ### Step 3: configure the middleware 122 | ```js 123 | import { createStore, applyMiddleware } from 'redux' 124 | import { thunkMiddleware } from '@rootstrap/redux-tools' 125 | 126 | import rootReducer from 'src/reducers/index' 127 | 128 | const store = createStore(rootReducer, applyMiddleware(thunkMiddleware)) 129 | ``` 130 | 131 | ## License 132 | 133 | **@rootstrap/redux-tools** is available under the MIT license. See the LICENSE file for more info. 134 | 135 | ## Credits 136 | 137 | **@rootstrap/redux-tools** is maintained by [Rootstrap](http://www.rootstrap.com) with the help of our [contributors](https://github.com/rootstrap/redux-tools/contributors). 138 | 139 | [](http://www.rootstrap.com) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rootstrap/redux-tools", 3 | "version": "1.0.0", 4 | "description": "Redux tools we use in both react bases", 5 | "main": "lib/index.js", 6 | "module": "src/index.js", 7 | "files": [ 8 | "src", 9 | "lib", 10 | "README.md" 11 | ], 12 | "scripts": { 13 | "test": "jest tests", 14 | "build": "rollup -c", 15 | "prepare": "rollup -c" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/rootstrap/redux-tools.git" 20 | }, 21 | "keywords": [ 22 | "redux", 23 | "rootstrap", 24 | "react" 25 | ], 26 | "author": "Pedro Zunino ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/rootstrap/redux-tools/issues" 30 | }, 31 | "homepage": "https://github.com/rootstrap/redux-tools#readme", 32 | "dependencies": { 33 | "immer": "^5.0.0" 34 | }, 35 | "devDependencies": { 36 | "rollup": "^1.29.0", 37 | "@babel/core": "^7.9.0", 38 | "@babel/preset-env": "^7.9.0", 39 | "babel-jest": "^25.1.0", 40 | "eslint": "^6.6.0", 41 | "eslint-config-prettier": "^6.5.0", 42 | "eslint-plugin-import": "^2.18.2", 43 | "eslint-plugin-prettier": "^3.1.1", 44 | "eslint-plugin-react-hooks": "^2.2.0", 45 | "jest": "^24.9.0", 46 | "prettier": "^1.19.1", 47 | "react-redux": "^7.1.1", 48 | "redux": "^4.0.4", 49 | "redux-mock-store": "^1.5.3" 50 | }, 51 | "peerDependencies": { 52 | "react": "^16.8.0", 53 | "redux": "^4.0.4", 54 | "react-redux": "^7.1.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | 3 | export default { 4 | input: 'src/index.js', 5 | external: ['immer', 'react-redux'], 6 | output: [ 7 | { file: pkg.main, format: 'cjs' }, 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const NOT_STARTED = 'NOT_STARTED' 2 | 3 | export const LOADING = 'LOADING' 4 | 5 | export const SUCCESS = 'SUCCESS' 6 | 7 | export const ERROR = 'ERROR' 8 | 9 | export const REQUEST = 'REQUEST' 10 | 11 | export const RESET = 'RESET' 12 | -------------------------------------------------------------------------------- /src/createAction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes action creators 3 | * 4 | * @param {string} type - Dispatched actions type 5 | * @return {function} Action creator 6 | * 7 | * @example 8 | * const loginSuccess = createAction('LOGIN_SUCCESS') 9 | */ 10 | 11 | export default type => { 12 | const action = payload => ({ 13 | type, 14 | payload, 15 | }) 16 | action.toString = () => type 17 | return action 18 | } 19 | -------------------------------------------------------------------------------- /src/createReducer.js: -------------------------------------------------------------------------------- 1 | import imm from 'immer' 2 | 3 | /** 4 | * Reducer creator util 5 | * 6 | * @param initialState Reducer initial state 7 | * @param actionHandlers - An object with all the reducer handlers 8 | * 9 | * @return {function} A reducer ready to use in createStore 10 | * 11 | * @example 12 | * const myReducer = createReducer({}, { 13 | * [loginSuccess]: (state, action) => { 14 | * state.user = action.payload 15 | * } 16 | * }) 17 | */ 18 | 19 | export default (initialState, actionHandlers) => ( 20 | state = initialState, 21 | action, 22 | ) => 23 | imm(state, draft => 24 | actionHandlers[action.type] 25 | ? actionHandlers[action.type](draft, action) 26 | : state, 27 | ) 28 | -------------------------------------------------------------------------------- /src/createThunk.js: -------------------------------------------------------------------------------- 1 | import { SUCCESS, ERROR, REQUEST, RESET } from './constants' 2 | import createAction from './createAction' 3 | 4 | /** 5 | * Creates different actions creators 6 | * 7 | * @param {string} actionName - Action name, will be used as a prefix for the action creators. 8 | * @param {function} thunk - This is your async thunk, receives all forwarded params and `dispatch` and `getState` as params 9 | * 10 | * @returns {ActionCreator} Action that can be dispatched to start the async thunk, can also be 11 | * deconstructed to get request, error, and success action creators (can be used as keys in reducer) 12 | * 13 | * @example 14 | * export const getProfile = createActionWithThunk( 15 | * 'LOGIN', 16 | * user => userService.login(user), 17 | * ); 18 | * export const { success, error } = getProfile; 19 | */ 20 | 21 | export default (actionName, thunk) => { 22 | const request = createAction(`${actionName}_${REQUEST}`) 23 | const error = createAction(`${actionName}_${ERROR}`) 24 | const success = createAction(`${actionName}_${SUCCESS}`) 25 | const reset = createAction(`${actionName}_${RESET}`) 26 | 27 | const action = (...params) => ({ 28 | success, 29 | error, 30 | thunk: (dispatch, getState) => thunk(...params, dispatch, getState), 31 | type: request.toString(), 32 | }) 33 | 34 | action.request = request 35 | action.error = error 36 | action.success = success 37 | action.reset = reset 38 | action.toString = () => actionName 39 | 40 | return action 41 | } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as createAction } from './createAction' 2 | export { default as createThunk } from './createThunk' 3 | export { default as createReducer } from './createReducer' 4 | export { default as statusReducer } from './statusReducer' 5 | export { default as thunkMiddleware } from './thunkMiddleware' 6 | export { default as useStatus } from './useStatus' 7 | export { default as useLoading } from './useLoading' 8 | export * from './constants' 9 | -------------------------------------------------------------------------------- /src/statusReducer.js: -------------------------------------------------------------------------------- 1 | import imm from 'immer' 2 | import { NOT_STARTED, LOADING, SUCCESS, ERROR } from './constants' 3 | 4 | const handleAction = (state, action) => { 5 | const { type, payload } = action 6 | 7 | const matchesStart = /(.*)_REQUEST/.exec(type) 8 | const matchesError = /(.*)_ERROR/.exec(type) 9 | const matchesReset = /(.*)_RESET/.exec(type) 10 | const matchesSuccess = /(.*)_SUCCESS/.exec(type) 11 | 12 | let status = NOT_STARTED 13 | let key = null 14 | 15 | if (matchesStart) { 16 | const [, requestName] = matchesStart 17 | key = requestName 18 | status = LOADING 19 | } else if (matchesReset) { 20 | const [, requestName] = matchesReset 21 | key = requestName 22 | status = NOT_STARTED 23 | } else if (matchesError) { 24 | const [, requestName] = matchesError 25 | key = requestName 26 | status = ERROR 27 | } else if (matchesSuccess) { 28 | const [, requestName] = matchesSuccess 29 | key = requestName 30 | status = SUCCESS 31 | } 32 | 33 | if (key) state[key] = { status, error: matchesError ? payload : undefined } 34 | 35 | return state 36 | } 37 | 38 | export default (state = {}, action) => 39 | imm(state, draft => handleAction(draft, action)) 40 | -------------------------------------------------------------------------------- /src/thunkMiddleware.js: -------------------------------------------------------------------------------- 1 | const id = item => item 2 | 3 | const thunkMiddlewareCreator = ({ 4 | parseError = id, 5 | parseResponse = id, 6 | } = {}) => ({ dispatch, getState }) => next => async action => { 7 | next(action) 8 | 9 | const { thunk, success, error } = action 10 | if (typeof thunk === 'function') { 11 | try { 12 | const response = await thunk(dispatch, getState) 13 | return dispatch(success(parseResponse(response))) 14 | } catch (err) { 15 | return dispatch(error(parseError(err))) 16 | } 17 | } 18 | } 19 | 20 | const thunkMiddleware = thunkMiddlewareCreator() 21 | thunkMiddleware.withConfig = thunkMiddlewareCreator 22 | 23 | export default thunkMiddleware 24 | -------------------------------------------------------------------------------- /src/useLoading.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { LOADING } from './constants' 3 | 4 | /** 5 | * useLoading hook 6 | * 7 | * @param {string} action Prefix for the action names 8 | * 9 | * @returns {boolean} Wether the action is loading 10 | * 11 | * @example 12 | * const isLoading = useStatus(getProfile) 13 | */ 14 | 15 | export default action => 16 | useSelector(({ statusReducer }) => { 17 | const { status } = statusReducer[action] || {} 18 | return status === LOADING 19 | }) 20 | -------------------------------------------------------------------------------- /src/useStatus.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | 3 | /** 4 | * useStatus hook 5 | * 6 | * @param {string} action Prefix for the action names 7 | * 8 | * @returns {object} Object with status and error keys 9 | * 10 | * @example 11 | * const { status, error } = useStatus(login) 12 | */ 13 | 14 | export default action => 15 | useSelector(({ statusReducer }) => { 16 | const { status, error } = statusReducer[action] || {} 17 | return { 18 | status, 19 | error, 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /tests/thunkMiddleware.spec.js: -------------------------------------------------------------------------------- 1 | import configureStore from 'redux-mock-store' 2 | 3 | import thunkMiddleware from '../src/thunkMiddleware' 4 | import createThunk from '../src/createThunk' 5 | 6 | const parseError = error => error.toString() 7 | const parseResponse = response => response.toUpperCase() 8 | let mockStore = configureStore([thunkMiddleware]) 9 | 10 | describe('actionThunk Middleware', () => { 11 | let store 12 | let mockErrorAction 13 | let mockSuccessAction 14 | 15 | const error = new Error('testing') 16 | const success = 'Yay!' 17 | 18 | beforeEach(() => { 19 | store = mockStore({}) 20 | mockErrorAction = createThunk('error', () => { 21 | throw error 22 | }) 23 | mockSuccessAction = createThunk('success', () => { 24 | return success 25 | }) 26 | }) 27 | 28 | describe('when dispatching any action', () => { 29 | it('the action should not be dismissed', () => { 30 | store.dispatch(mockSuccessAction()) 31 | const actions = store.getActions() 32 | expect(actions[0]).toHaveProperty( 33 | 'type', 34 | mockSuccessAction.request.toString(), 35 | ) 36 | }) 37 | }) 38 | 39 | describe('when dispatching the action that fails', () => { 40 | it('should dispatch the error action', async () => { 41 | await store.dispatch(mockErrorAction()) 42 | const actions = store.getActions() 43 | expect(actions).toContainEqual(mockErrorAction.error(error)) 44 | }) 45 | }) 46 | 47 | describe('when dispatching the action that succeeds', () => { 48 | it('should dispatch the success action', async () => { 49 | await store.dispatch(mockSuccessAction()) 50 | const actions = store.getActions() 51 | expect(actions).toContainEqual(mockSuccessAction.success(success)) 52 | }) 53 | }) 54 | 55 | describe('when adding a parsing functions', () => { 56 | let mockStoreWithParseFunctions 57 | beforeEach(() => { 58 | mockStoreWithParseFunctions = configureStore([ 59 | thunkMiddleware.withConfig({ parseError, parseResponse }), 60 | ])({}) 61 | mockErrorAction = createThunk('error', () => { 62 | throw error 63 | }) 64 | mockSuccessAction = createThunk('success', () => { 65 | return success 66 | }) 67 | }) 68 | 69 | describe('when dispatching the action that fails', () => { 70 | it('should dispatch the error action with the parsed error', async () => { 71 | await mockStoreWithParseFunctions.dispatch(mockErrorAction()) 72 | const actions = mockStoreWithParseFunctions.getActions() 73 | expect(actions).toContainEqual(mockErrorAction.error(parseError(error))) 74 | }) 75 | }) 76 | 77 | describe('when dispatching the action that succeeds', () => { 78 | it('should dispatch the success action with the parsed data', async () => { 79 | await mockStoreWithParseFunctions.dispatch(mockSuccessAction()) 80 | const actions = mockStoreWithParseFunctions.getActions() 81 | expect(actions).toContainEqual( 82 | mockSuccessAction.success(parseResponse(success)), 83 | ) 84 | }) 85 | }) 86 | }) 87 | }) 88 | --------------------------------------------------------------------------------