├── .babelrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist └── index.js ├── package-lock.json ├── package.json └── src ├── __tests__ ├── __snapshots__ │ ├── redux-ledger-spec.js.md │ └── redux-ledger-spec.js.snap └── redux-ledger-spec.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { "modules": "umd" }]], 3 | "plugins": [ 4 | ["transform-object-rest-spread", { "useBuiltIns": true }] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | .DS_Store 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | .nyc_output 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Internal npm setup 39 | .npmrc 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | .idea/ 44 | 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | install: 5 | - npm install 6 | script: "npm test" 7 | after_success: "npm run coverage" 8 | cache: 9 | directories: 10 | - node_modules 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wayfair 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/wayfair/redux-ledger.svg?branch=master)](https://travis-ci.org/wayfair/redux-ledger) 2 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 3 | [![codecov](https://codecov.io/gh/wayfair/hypernova-php/branch/master/graph/badge.svg)](https://codecov.io/gh/wayfair/redux-ledger) 4 | 5 | # redux-ledger 6 | 7 | Tiny redux middleware to run integration tests with thunks! 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm install --save-dev redux-ledger 13 | ``` 14 | 15 | ## Usage 16 | 17 | Add `ledger` middleware to your Redux store. Simulate events and dispatch 18 | just as you would before. It will intercept and record _all_ actions. Once you 19 | are ready, use `ledger.resolve()` to wait for any pending 20 | thunks to complete, then run your assertions. 21 | 22 | **Note** - For accurate results place `ledger` as the first middleware in the store. 23 | 24 | ### Recording Actions 25 | 26 | Every action in the store can be recorded by adding the middleware to the store. 27 | 28 | ```js 29 | import makeLedger from "redux-ledger"; 30 | import { createStore, applyMiddleware } from "redux"; 31 | import thunk from "redux-thunk"; 32 | 33 | const doA = payload => ({ type: "action_a", payload }); 34 | const doB = payload => ({ type: "action_b", payload }); 35 | 36 | // For the purposes of a demo these are defined inline ... 37 | const doAsyncA = payload => dispatch => { 38 | return Promise.resolve().then(() => { 39 | dispatch(doA(payload)); 40 | }); 41 | }; 42 | 43 | // ... but let's pretend they are some long running requests, mocked or not 44 | const doAsyncB = payload => dispatch => { 45 | return Promise.resolve().then(() => { 46 | dispatch(doB(payload)); 47 | }); 48 | }; 49 | 50 | test("asynchronous actions fired from my App", () => { 51 | const ledger = makeLedger(); 52 | const store = createStore( 53 | (state = {}) => state, 54 | applyMiddleware(ledger, thunk) 55 | ); 56 | store.dispatch(doA({ foo: "foo" })); 57 | store.dispatch(doB({ bar: "bar" })); 58 | // ledger.getActions() to get all actions recorded 59 | expect(ledger.getActions()).toMatchSnapshot(); 60 | }); 61 | ``` 62 | 63 | ### Complex Example 64 | 65 | `redux-ledger` shines when you want to unit test an entire component connected to 66 | a store. 67 | 68 | ```js 69 | import makeLedger from "redux-ledger"; 70 | import { createStore, applyMiddleware } from "redux"; 71 | import { Provider } from "react-redux"; 72 | import thunk from "redux-thunk"; 73 | import { mount } from "enzyme"; 74 | // Component to test 75 | import MyAppContainer from "my-app-container"; 76 | 77 | test("asynchronous actions fired from my App", () => { 78 | const ledger = makeLedger(); 79 | const store = createStore(reducer, applyMiddleware(ledger, thunk)); 80 | const wrapper = mount( 81 | 82 | 83 | 84 | ); 85 | 86 | // Simulate user interaction which will kick off asynchronous actions 87 | wrapper.find(MyAppContainer).simulate("click"); 88 | 89 | return ledger.resolve().then(actions => { 90 | expect(actions).toMatchSnapshot(); 91 | }); 92 | }); 93 | ``` 94 | 95 | ## API 96 | 97 | #### `createLedger()` - Factory 98 | 99 | You'll want a new ledger for each new store, as ledgers should 100 | be scoped to a single store. Returns a `ledger` middleware instance. 101 | 102 | #### `ledger` - Instance 103 | 104 | The middleware function. Has additional methods on the function object: 105 | 106 | ```js 107 | // Will wait for any pending promise from actions to finish. 108 | // Resolves with the array of actions dispatched 109 | ledger.resolve().then(actions => { ... }); 110 | 111 | // Will return all actions recorded so far 112 | ledger.getActions(); // [ { type: 'a', ...}, {type: 'b', ...} ] 113 | 114 | // Will clear any previously recorded actions 115 | ledger.clearActions(); 116 | ``` 117 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | (function(global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define(["module"], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(module); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod); 11 | global.index = mod.exports; 12 | } 13 | })(this, function(module) { 14 | "use strict"; 15 | 16 | module.exports = function makeLedger() { 17 | var promises = []; 18 | var actions = []; 19 | 20 | // Wrap thunk, call and save result 21 | var wrap = function wrap(action) { 22 | return function() { 23 | var result = action.apply(undefined, arguments); 24 | promises.push(result); 25 | 26 | return result; 27 | }; 28 | }; 29 | 30 | // Middleware 31 | var ledger = function ledger() { 32 | return function(next) { 33 | return function(action) { 34 | if (typeof action === "function") { 35 | return next(wrap(action)); 36 | } 37 | actions.push(action); 38 | return next(action); 39 | }; 40 | }; 41 | }; 42 | 43 | // Return actions, don't allow mutation 44 | ledger.getActions = function() { 45 | return [].concat(actions); 46 | }; 47 | 48 | // Clear all actions return what we _had_ 49 | ledger.clearActions = function() { 50 | return actions.splice(0); 51 | }; 52 | 53 | // Recurse into pending promises returned from thunks 54 | ledger.resolve = function() { 55 | return ( 56 | // Not immutable for sure, but it's an implementation detail so w/e 57 | Promise.all(promises.splice(0)).then(function() { 58 | // More promises were added after a thunk? Recurse 59 | if (promises.length) { 60 | return ledger.resolve(); 61 | } 62 | 63 | // Return all actions back to user 64 | return actions; 65 | }) 66 | ); 67 | }; 68 | 69 | return ledger; 70 | }; 71 | }); 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-ledger", 3 | "version": "1.2.3", 4 | "description": "Redux unit test middleware", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "cross-env BABEL_ENV=production BABEL_OUTPUT=umd babel src --out-dir dist/ --ignore __tests__ --copy-files", 8 | "test": "nyc --cache ava --no-color --tap | tap-nyc", 9 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 10 | "tdd": "ava", 11 | "nyc": "nyc", 12 | "debug": "node --inspect-brk node_modules/ava/profile.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:wayfair/redux-ledger.git" 17 | }, 18 | "keywords": [ 19 | "redux", 20 | "unit-test", 21 | "middleware", 22 | "thunk" 23 | ], 24 | "author": "Arthur Buldauskas", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "ava": "0.25.0", 28 | "babel-plugin-transform-object-rest-spread": "6.26.0", 29 | "babel-preset-env": "^1.7.0", 30 | "codecov": "^3.1.0", 31 | "cross-env": "5.1.4", 32 | "nyc": "^13.2.0", 33 | "prettier": "1.11.1", 34 | "redux": "3.7.2", 35 | "redux-thunk": "2.2.0", 36 | "tap-nyc": "1.0.3" 37 | }, 38 | "ava": { 39 | "files": [ 40 | "src/**/*[sS]pec.js" 41 | ], 42 | "source": [ 43 | "src/**/*.js", 44 | "src/**/*.json" 45 | ], 46 | "concurrency": 5, 47 | "failFast": true, 48 | "failWithoutAssertions": false, 49 | "tap": false, 50 | "powerAssert": false, 51 | "modules": false, 52 | "babel": "inherit" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/redux-ledger-spec.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/__tests__/redux-ledger-spec.js` 2 | 3 | The actual snapshot is saved in `redux-ledger-spec.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## it records actions in a vanilla redux store 8 | 9 | > Snapshot 1 10 | 11 | [ 12 | { 13 | payload: { 14 | foo: 'foo', 15 | }, 16 | type: 'action_a', 17 | }, 18 | { 19 | payload: { 20 | bar: 'bar', 21 | }, 22 | type: 'action_b', 23 | }, 24 | 25 | 26 | ## waits for async actions 27 | 28 | > Snapshot 1 29 | 30 | [ 31 | { 32 | payload: { 33 | foo: 'foo', 34 | }, 35 | type: 'action_a', 36 | }, 37 | { 38 | payload: { 39 | bar: 'bar', 40 | }, 41 | type: 'action_b', 42 | }, 43 | { 44 | payload: { 45 | foo: 'fooz', 46 | }, 47 | type: 'action_a', 48 | }, 49 | { 50 | payload: { 51 | bar: 'baz', 52 | }, 53 | type: 'action_b', 54 | }, 55 | 56 | 57 | ## getActions 58 | 59 | > Snapshot 1 60 | 61 | [ 62 | { 63 | payload: { 64 | foo: 'foooooo-bar', 65 | }, 66 | type: 'action_a', 67 | }, 68 | 69 | 70 | ## clearActions 71 | 72 | > Snapshot 1 73 | 74 | [] -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/redux-ledger-spec.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wayfair-archive/redux-ledger/6e9ab99fb5a86643c650add7522f16941b67352c/src/__tests__/__snapshots__/redux-ledger-spec.js.snap -------------------------------------------------------------------------------- /src/__tests__/redux-ledger-spec.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import makeLedger from ".."; 3 | import { createStore, applyMiddleware } from "redux"; 4 | import thunk from "redux-thunk"; 5 | 6 | const reducer = (state = { foo: null, bar: null }, { type, payload }) => { 7 | switch (type) { 8 | case "action_a": 9 | return { ...state, foo: payload.foo }; 10 | case "action_b": 11 | return { ...state, bar: payload.bar }; 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | const doA = payload => ({ type: "action_a", payload }); 18 | const doB = payload => ({ type: "action_b", payload }); 19 | 20 | const doAsyncA = payload => dispatch => { 21 | return Promise.resolve().then(() => { 22 | dispatch(doA(payload)); 23 | }); 24 | }; 25 | 26 | const doAsyncB = payload => dispatch => { 27 | return Promise.resolve().then(() => { 28 | dispatch(doB(payload)); 29 | }); 30 | }; 31 | 32 | const doAsyncC = ({ foo, bar }) => dispatch => { 33 | return Promise.resolve().then(() => { 34 | dispatch(doAsyncA({ foo })); 35 | dispatch(doAsyncB({ bar })); 36 | }); 37 | }; 38 | 39 | test("it records actions in a vanilla redux store", t => { 40 | const ledger = makeLedger(); 41 | const store = createStore(reducer, applyMiddleware(ledger, thunk)); 42 | store.dispatch(doA({ foo: "foo" })); 43 | store.dispatch(doB({ bar: "bar" })); 44 | t.snapshot(ledger.getActions()); 45 | }); 46 | 47 | test("waits for async actions", t => { 48 | const ledger = makeLedger(); 49 | const store = createStore(reducer, applyMiddleware(ledger, thunk)); 50 | store.dispatch(doAsyncA({ foo: "foo" })); 51 | // Here are a bunch of cascading actions 52 | // Something that you will run into if you're trying to simulate UI events in 53 | // React one after another 54 | return ledger 55 | .resolve() 56 | .then(() => { 57 | store.dispatch(doAsyncB({ bar: "bar" })); 58 | return ledger.resolve(); 59 | }) 60 | .then(() => { 61 | store.dispatch(doAsyncC({ foo: "fooz", bar: "baz" })); 62 | return ledger.resolve(); 63 | }) 64 | .then(actions => { 65 | t.snapshot(actions); 66 | }); 67 | }); 68 | 69 | test("getActions", t => { 70 | const ledger = makeLedger(); 71 | const store = createStore(reducer, applyMiddleware(ledger, thunk)); 72 | store.dispatch(doA({ foo: "foooooo-bar" })); 73 | t.snapshot(ledger.getActions()); 74 | }); 75 | 76 | test("clearActions", t => { 77 | const ledger = makeLedger(); 78 | const store = createStore(reducer, applyMiddleware(ledger, thunk)); 79 | store.dispatch(doA({ foo: "foooooo-bar" })); 80 | ledger.clearActions(); 81 | t.snapshot(ledger.getActions()); 82 | }); 83 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function makeLedger() { 2 | const promises = []; 3 | const actions = []; 4 | 5 | // Wrap thunk, call and save result 6 | const wrap = action => (...args) => { 7 | const result = action(...args); 8 | promises.push(result); 9 | 10 | return result; 11 | }; 12 | 13 | // Middleware 14 | const ledger = () => next => action => { 15 | if (typeof action === "function") { 16 | return next(wrap(action)); 17 | } 18 | actions.push(action); 19 | return next(action); 20 | }; 21 | 22 | // Return actions, don't allow mutation 23 | ledger.getActions = () => [...actions]; 24 | 25 | // Clear all actions return what we _had_ 26 | ledger.clearActions = () => actions.splice(0); 27 | 28 | // Recurse into pending promises returned from thunks 29 | ledger.resolve = () => 30 | // Not immutable for sure, but it's an implementation detail so w/e 31 | Promise.all(promises.splice(0)).then(() => { 32 | // More promises were added after a thunk? Recurse 33 | if (promises.length) { 34 | return ledger.resolve(); 35 | } 36 | 37 | // Return all actions back to user 38 | return actions; 39 | }); 40 | 41 | return ledger; 42 | }; 43 | --------------------------------------------------------------------------------