├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .nvmrc ├── README.md ├── flow ├── chai.js ├── mocha_v2.4.x.js └── sinon.js ├── package.json ├── src ├── combineReducers.js ├── createLogger.js ├── createStore.js ├── index.js └── types.js ├── test.setup.js └── test ├── combineReducers.test.js ├── createLogger.test.js ├── createStore.test.js └── fixtures ├── actionCreators.js ├── actionTypes.js └── reducers.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/**/* 2 | flow/**/* 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard" 5 | ], 6 | "rules": { 7 | "comma-dangle": ["error", "always-multiline"], 8 | "semi": ["error", "always"] 9 | }, 10 | "env": { 11 | "browser": true, 12 | "node": true, 13 | "mocha": true 14 | }, 15 | "globals": { 16 | "expect": true, 17 | "sinon": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [libs] 2 | flow 3 | 4 | [options] 5 | esproposal.class_instance_fields=enable 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log 4 | .eslintcache 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.2.1 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `redux-mori` 2 | 3 | [![NPM version](http://img.shields.io/npm/v/redux-mori.svg?style=flat-square)](https://www.npmjs.org/package/redux-mori) 4 | 5 | `redux-mori` is a drop-in replacement for Redux's [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html) and [`createStore`](http://redux.js.org/docs/api/createStore.html) that works with [`mori.js`](http://swannodette.github.io/mori) immutable data structures. 6 | 7 | Any `preloadedState` for Redux's [`createStore`](https://github.com/reactjs/redux/blob/master/docs/api/createStore.md) should be a `mori` [`hashMap`](http://swannodette.github.io/mori/#hashMap). 8 | 9 | ## Usage 10 | 11 | If you create a store with `preloadedState`, make sure it is an instance of [`hashMap`]((http://swannodette.github.io/mori/#hashMap)): 12 | 13 | #### Create Store / Combine Reducers: 14 | ```js 15 | import { toClj } from 'mori'; 16 | import { combineReducers, createStore } from 'redux-mori'; 17 | import fooReducer from './fooReducer'; 18 | import bazReducer from './bazReducer'; 19 | 20 | const preloadedState = toClj({foo: 'bar', baz: 'quux'}); 21 | const rootReducer = combineReducers({fooReducer, bazReducer}); 22 | const store = createStore(rootReducer, preloadedState); 23 | ``` 24 | 25 | #### Action: 26 | ```js 27 | const ACTION_REQUEST = 'ACTION_REQUEST' 28 | const actionRequest = () => hashMap('type', ACTION_REQUEST) 29 | ``` 30 | 31 | Please note you can still use original redux `createStore` and write actions as JS objects, even if you're using `mori` in your state. 32 | 33 | ## Logger 34 | If you're using Redux Logger and would like to get readable logs of state and actions, you can use our `createLogger` wrapper. You can use it exactly in the same way as the original but it presets following values: 35 | 36 | ```js 37 | { 38 | actionTransformer, 39 | collapsed: true, 40 | stateTransformer, 41 | } 42 | ``` 43 | 44 | Example: 45 | 46 | ```js 47 | import createLogger from 'redux-mori/dist/createLogger'; 48 | import { createStore } from 'redux-mori'; 49 | import { applyMiddleware } from 'redux'; 50 | const middlewares = []; 51 | if (isDev) { 52 | const logger = createLogger({ 53 | predicate: (getState, action) => !/EFFECT_/.test(action.type), 54 | }); 55 | middlewares.push(logger); 56 | } 57 | 58 | const store = createStore(rootReducer, applyMiddleware(...middlewares)); 59 | ``` 60 | 61 | Actions get transformed fully into JS if they are `mori` objects. If they are JS objects with only some values as `mori` structures, it will transform this data into JS as well. 62 | -------------------------------------------------------------------------------- /flow/chai.js: -------------------------------------------------------------------------------- 1 | declare var expect: Function; 2 | -------------------------------------------------------------------------------- /flow/mocha_v2.4.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: c7f438528a77dbf9c0b35cf473abd5d4 2 | // flow-typed version: 668d1ec92a/mocha_v2.4.x/flow_>=v0.22.x 3 | 4 | type TestFunction = (() => void | Promise) | ((done : () => void) => void); 5 | 6 | declare var describe : { 7 | (name:string, spec:() => void): void; 8 | only(description:string, spec:() => void): void; 9 | skip(description:string, spec:() => void): void; 10 | timeout(ms:number): void; 11 | }; 12 | 13 | declare var context : typeof describe; 14 | 15 | declare var it : { 16 | (name:string, spec:TestFunction): void; 17 | only(description:string, spec:TestFunction): void; 18 | skip(description:string, spec:TestFunction): void; 19 | timeout(ms:number): void; 20 | }; 21 | 22 | declare function before(method : TestFunction):void; 23 | declare function beforeEach(method : TestFunction):void; 24 | declare function after(method : TestFunction):void; 25 | declare function afterEach(method : TestFunction):void; 26 | -------------------------------------------------------------------------------- /flow/sinon.js: -------------------------------------------------------------------------------- 1 | declare var sinon: Function; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-mori", 3 | "version": "0.1.0", 4 | "description": "Drop-in replacement for Redux combineReducers and createStore that works with mori.js immutable data structures", 5 | "main": "./dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "build": "cross-env BABEL_ENV=commonjs babel src --out-dir dist", 11 | "check": "npm run lint && npm test", 12 | "clean": "rimraf dist", 13 | "lint": "eslint --cache src", 14 | "prepublish": "npm run clean && npm run check && npm run build", 15 | "test:watch": "npm test -- --watch", 16 | "test": "mocha --compilers js:babel-register --require test.setup" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/redbadger/redux-mori.git" 21 | }, 22 | "keywords": [ 23 | "redux", 24 | "immutable", 25 | "mori" 26 | ], 27 | "authors": [ 28 | "Stuart Harris (https://github.com/stuartharris)", 29 | "Anna Doubkova (https://github.com/lithin)" 30 | ], 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/redbadger/redux-mori/issues" 34 | }, 35 | "homepage": "https://github.com/redbadger/redux-mori#readme", 36 | "dependencies": { 37 | "mori": "^0.3.2", 38 | "redux-logger": "^2.6.1" 39 | }, 40 | "devDependencies": { 41 | "babel-cli": "^6.9.0", 42 | "babel-core": "^6.9.1", 43 | "babel-eslint": "^6.0.4", 44 | "babel-loader": "^6.2.4", 45 | "babel-plugin-module-alias": "^1.4.0", 46 | "babel-polyfill": "^6.9.1", 47 | "babel-preset-es2015": "^6.9.0", 48 | "babel-preset-react": "^6.5.0", 49 | "babel-preset-stage-0": "^6.5.0", 50 | "babel-register": "^6.9.0", 51 | "chai": "^3.5.0", 52 | "cross-env": "^1.0.8", 53 | "eslint": "^2.12.0", 54 | "eslint-config-standard": "^5.3.1", 55 | "eslint-plugin-promise": "^1.3.2", 56 | "eslint-plugin-standard": "^1.3.2", 57 | "mocha": "^2.5.3", 58 | "rimraf": "^2.5.2", 59 | "sinon": "^1.17.4", 60 | "sinon-chai": "^2.8.0", 61 | "webpack": "^1.13.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/combineReducers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { get, hashMap, merge } from 'mori'; 3 | import type { State, Action, Reducer, Reducers } from './types'; 4 | 5 | function combineReducers (reducers: Reducers) : Reducer { 6 | const reducerKeys = Object.keys(reducers); 7 | return function (state: ?State, action: Action) : State { 8 | return reducerKeys.reduce((acc: State, key: string): State => { 9 | const reducer = reducers[key]; 10 | const currentDomainState: ?State = get(state, key, undefined); 11 | const nextDomainState: State = reducer(currentDomainState, action); 12 | return merge(acc, hashMap(key, nextDomainState)); 13 | }, state || hashMap()); 14 | }; 15 | }; 16 | 17 | export default combineReducers; 18 | -------------------------------------------------------------------------------- /src/createLogger.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import reduxLogger from 'redux-logger'; 4 | import { toJs, isCollection } from 'mori'; 5 | 6 | export const stateTransformer = (state: Object): Object => toJs(state); 7 | export const actionTransformer = (action: Object): Object => { 8 | if (isCollection(action)) { 9 | return toJs(action); 10 | } 11 | 12 | return Object.keys(action) 13 | .reduce((acc, name) => ({ 14 | ...acc, 15 | [name]: isCollection(action[name]) ? toJs(action[name]) : action[name], 16 | }), {}); 17 | }; 18 | 19 | const getDefaultOptions = (): Object => ({ 20 | actionTransformer, 21 | collapsed: true, 22 | stateTransformer, 23 | }); 24 | 25 | const createLogger = (options: Object, logger: Function = reduxLogger): Object => logger({ // eslint-disable-line space-infix-ops 26 | ...getDefaultOptions(), 27 | ...options, 28 | }); 29 | 30 | export default createLogger; 31 | -------------------------------------------------------------------------------- /src/createStore.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isMap, get, hashMap } from 'mori'; 3 | import type { State, Action, Reducer, StoreEnhancer } from './types'; 4 | 5 | export const ActionTypes = hashMap('INIT', '@@redux/INIT'); 6 | 7 | export default function createStore (reducer: Reducer, preloadedState: ?State, enhancer: ?StoreEnhancer) { 8 | if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { 9 | enhancer = preloadedState; 10 | preloadedState = undefined; 11 | } 12 | 13 | if (typeof enhancer !== 'undefined') { 14 | if (typeof enhancer !== 'function') { 15 | throw new Error('Expected the enhancer to be a function.'); 16 | } 17 | 18 | return enhancer(createStore)(reducer, preloadedState); 19 | } 20 | 21 | if (typeof reducer !== 'function') { 22 | throw new Error('Expected the reducer to be a function.'); 23 | } 24 | 25 | let currentReducer = reducer; 26 | let currentState = preloadedState; 27 | let currentListeners = []; 28 | let nextListeners = currentListeners; 29 | let isDispatching = false; 30 | 31 | const ensureCanMutateNextListeners = () => { 32 | if (nextListeners === currentListeners) { 33 | nextListeners = currentListeners.slice(); 34 | } 35 | }; 36 | 37 | const getState = (): ?State => currentState; 38 | 39 | const subscribe = (listener: Function) => { 40 | if (typeof listener !== 'function') { 41 | throw new Error('Expected listener to be a function.'); 42 | } 43 | 44 | let isSubscribed = true; 45 | 46 | ensureCanMutateNextListeners(); 47 | nextListeners.push(listener); 48 | 49 | return () => { 50 | if (!isSubscribed) { 51 | return; 52 | } 53 | 54 | isSubscribed = false; 55 | 56 | ensureCanMutateNextListeners(); 57 | const index = nextListeners.indexOf(listener); 58 | nextListeners.splice(index, 1); 59 | }; 60 | }; 61 | 62 | function dispatch (action: Action) { 63 | if (!isMap(action)) { 64 | throw new Error( 65 | 'Actions must be maps. ' + 66 | 'Use custom middleware for async actions.' 67 | ); 68 | } 69 | 70 | if (typeof get(action, 'type') === 'undefined') { 71 | throw new Error( 72 | 'Actions may not have an undefined "type" property. ' + 73 | 'Have you misspelled a constant?' 74 | ); 75 | } 76 | 77 | if (isDispatching) { 78 | throw new Error('Reducers may not dispatch actions.'); 79 | } 80 | 81 | try { 82 | isDispatching = true; 83 | currentState = currentReducer(currentState, action); 84 | } finally { 85 | isDispatching = false; 86 | } 87 | 88 | var listeners = currentListeners = nextListeners; 89 | for (var i = 0; i < listeners.length; i++) { 90 | listeners[i](); 91 | } 92 | 93 | return action; 94 | } 95 | 96 | function replaceReducer (nextReducer: Reducer) { 97 | if (typeof nextReducer !== 'function') { 98 | throw new Error('Expected the nextReducer to be a function.'); 99 | } 100 | 101 | currentReducer = nextReducer; 102 | dispatch(hashMap('type', get(ActionTypes, 'INIT'))); 103 | } 104 | 105 | dispatch(hashMap('type', get(ActionTypes, 'INIT'))); 106 | 107 | return { 108 | dispatch, 109 | subscribe, 110 | getState, 111 | replaceReducer, 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export createStore from './createStore'; 2 | export combineReducers from './combineReducers'; 3 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export type State = {[key: string]: any}; 3 | export type Action = () => any; 4 | export type Dispatch = (action: Action) => Action; 5 | export type Reducer = (state: ?State, action: Action) => State; 6 | export type Reducers = { [key: string]: Reducer }; 7 | export type StoreCreator = (reducer: Reducer, preloadedState: ?State) => Store; 8 | export type StoreEnhancer = (next: StoreCreator) => StoreCreator; 9 | export type Store = { 10 | dispatch: Dispatch, 11 | getState: () => ?State, 12 | subscribe: (listener: () => void) => () => void, 13 | replaceReducer: (reducer: Reducer) => void, 14 | }; 15 | -------------------------------------------------------------------------------- /test.setup.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import chai from 'chai'; 3 | import sinon from 'sinon'; 4 | import sinonChai from 'sinon-chai'; 5 | 6 | chai.use(sinonChai); 7 | 8 | global.expect = chai.expect; 9 | global.sinon = sinon; 10 | -------------------------------------------------------------------------------- /test/combineReducers.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { toClj, hashMap, getIn, updateIn, inc } from 'mori'; 3 | import combineReducers from '../src/combineReducers'; 4 | import type { State } from '../src/types'; 5 | 6 | describe('combineReducers()', () => { 7 | context('reducer returns received state', () => { 8 | it('returns initial state', () => { 9 | const rootReducer = combineReducers({ 10 | foo: (state: ?State) => { 11 | return state || hashMap(); 12 | }, 13 | }); 14 | 15 | const initialState = toClj({ 16 | foo: { 17 | count: 1, 18 | }, 19 | }); 20 | 21 | const actual = rootReducer(initialState, hashMap('type', 'NONE')); 22 | expect(actual.toString()).to.equal(initialState.toString()); 23 | }); 24 | }); 25 | 26 | context('reducer creates new domain state', () => { 27 | it('returns new state', () => { 28 | const rootReducer = combineReducers({ 29 | foo: (state: ?State) => { 30 | return updateIn(state, ['count'], inc); 31 | }, 32 | }); 33 | 34 | const initialState = toClj({ 35 | foo: { 36 | count: 0, 37 | }, 38 | }); 39 | 40 | const actual = rootReducer(initialState, hashMap('type', 'ADD')); 41 | expect(getIn(actual, ['foo', 'count'])).to.equal(1); 42 | }); 43 | }); 44 | 45 | context('root reducer is created from nested combineReducers', () => { 46 | it('returns initial state from default values', () => { 47 | const initialState: State = toClj({ 48 | outer: { 49 | inner: { 50 | bar: false, 51 | foo: true, 52 | }, 53 | }, 54 | }); 55 | 56 | const innerDefaultState: State = toClj({ 57 | bar: false, 58 | foo: true, 59 | }); 60 | 61 | const rootReducer = combineReducers({ 62 | outer: combineReducers({ 63 | inner: (state : ? State = innerDefaultState) => { 64 | return state || hashMap(); 65 | }, 66 | }), 67 | }); 68 | 69 | expect(rootReducer(undefined, hashMap('type', 'WHATEVS')).toString()).to.eql(initialState.toString()); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/createLogger.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { hashMap, toJs } from 'mori'; 3 | import createLogger, { 4 | actionTransformer, 5 | stateTransformer, 6 | } from '../src/createLogger'; 7 | 8 | describe('create redux logger', () => { 9 | context('create logger', () => { 10 | it('merges default options with options from argument, creating redux logger', () => { 11 | const logger = sinon.spy(); 12 | const options = { 13 | collapsed: false, 14 | another: 'option', 15 | }; 16 | const expectedOptions = { 17 | actionTransformer, 18 | stateTransformer, 19 | collapsed: false, 20 | another: 'option', 21 | }; 22 | createLogger(options, logger); 23 | expect(logger).to.have.been.calledWith(expectedOptions); 24 | }); 25 | }); 26 | 27 | context('state transformation', () => { 28 | it('transforms state into js', () => { 29 | const initial = hashMap( 30 | 'user', hashMap( 31 | 'id', '123', 32 | 'name', 'Kadi' 33 | ), 34 | 'profile', hashMap( 35 | 'address', hashMap( 36 | 'country', hashMap( 37 | 'code', 'ET', 38 | 'name', 'Estonia' 39 | ) 40 | ) 41 | ) 42 | ); 43 | const expected = toJs(initial); 44 | const actual = stateTransformer(initial); 45 | expect(actual).to.deep.equal(expected); 46 | }); 47 | }); 48 | 49 | context('action transformation', () => { 50 | it('transforms action values to JS if they are a mori collection', () => { 51 | const initial = { 52 | type: 'LOGIN_SUCCESS', 53 | user: hashMap( 54 | 'id', '345', 55 | 'name', 'Marcel', 56 | 'language', hashMap( 57 | 'code', 'en', 58 | 'name', 'English' 59 | ) 60 | ), 61 | }; 62 | const expected = { 63 | type: 'LOGIN_SUCCESS', 64 | user: toJs(initial.user), 65 | }; 66 | const actual = actionTransformer(initial); 67 | expect(actual).to.deep.equal(expected); 68 | }); 69 | 70 | it('transforms actions into JS if they are a mori collection', () => { 71 | const initial = hashMap( 72 | 'type', 'LOGIN_SUCCESS', 73 | 'user', hashMap( 74 | 'id', '345', 75 | 'name', 'Marcel', 76 | 'language', hashMap( 77 | 'code', 'en', 78 | 'name', 'English' 79 | ) 80 | ), 81 | ); 82 | const expected = toJs(initial); 83 | const actual = actionTransformer(initial); 84 | expect(actual).to.deep.equal(expected); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/createStore.test.js: -------------------------------------------------------------------------------- 1 | import { equals, hashMap, vector } from 'mori'; 2 | import createStore from '../src/createStore'; 3 | import combineReducers from '../src/combineReducers'; 4 | import * as reducers from './fixtures/reducers'; 5 | import { addTodo, unknownAction } from './fixtures/actionCreators'; 6 | 7 | describe('createStore()', () => { 8 | context('initialized', () => { 9 | it('exposes the public API', () => { 10 | const rootReducer = combineReducers(reducers); 11 | const store = createStore(rootReducer); 12 | 13 | expect(store).to.have.all.keys( 14 | 'subscribe', 15 | 'dispatch', 16 | 'getState', 17 | 'replaceReducer' 18 | ); 19 | }); 20 | }); 21 | 22 | context('reducer argument', () => { 23 | it('throws when none', () => { 24 | expect(() => 25 | createStore() 26 | ).to.throw(); 27 | }); 28 | 29 | it('throws when string', () => { 30 | expect(() => 31 | createStore('') 32 | ).to.throw(); 33 | }); 34 | 35 | it('throws when map', () => { 36 | expect(() => 37 | createStore(hashMap()) 38 | ).to.throw(); 39 | }); 40 | 41 | it('throws when vector', () => { 42 | expect(() => 43 | createStore(vector()) 44 | ).to.throw(); 45 | }); 46 | 47 | it('does not throw when function', () => { 48 | expect(() => 49 | createStore(() => hashMap()) 50 | ).to.not.throw(); 51 | }); 52 | }); 53 | 54 | context('initial state and action arguments', () => { 55 | it('should return correct vector', () => { 56 | const store = createStore(reducers.todos, vector( 57 | hashMap('id', 1, 'text', 'foo') 58 | )); 59 | const expected = vector( 60 | hashMap('id', 1, 'text', 'foo') 61 | ); 62 | 63 | expect(equals(store.getState(), expected)).to.be.true; 64 | }); 65 | }); 66 | 67 | context('applying reducer to initial action', () => { 68 | it('should return correct initial state', () => { 69 | const store = createStore(reducers.todos, vector( 70 | hashMap('id', 1, 'text', 'Hello') 71 | )); 72 | const expected = vector( 73 | hashMap('id', 1, 'text', 'Hello') 74 | ); 75 | 76 | expect(equals(store.getState(), expected)).to.be.true; 77 | }); 78 | }); 79 | 80 | context('applying reducers to previous state', () => { 81 | it('should return correct state', () => { 82 | const store = createStore(reducers.todos); 83 | 84 | expect(equals(store.getState(), vector())).to.be.true; 85 | store.dispatch(unknownAction()); 86 | 87 | expect(equals(store.getState(), vector())).to.be.true; 88 | store.dispatch(addTodo('Hello')); 89 | 90 | expect(equals(store.getState(), vector(hashMap( 91 | 'id', 1, 92 | 'text', 'Hello' 93 | )))).to.be.true; 94 | store.dispatch(addTodo('World')); 95 | 96 | expect(equals(store.getState(), vector( 97 | hashMap( 98 | 'id', 1, 99 | 'text', 'Hello' 100 | ), 101 | hashMap( 102 | 'id', 2, 103 | 'text', 'World' 104 | )))).to.be.true; 105 | }); 106 | }); 107 | 108 | context('replacing a reducer', () => { 109 | it('should preserve state', () => { 110 | const store = createStore(reducers.todos); 111 | store.dispatch(addTodo('Hello')); 112 | store.dispatch(addTodo('World')); 113 | 114 | store.replaceReducer(reducers.todosReverse); 115 | 116 | expect(equals(store.getState(), vector( 117 | hashMap('id', 1, 'text', 'Hello'), 118 | hashMap('id', 2, 'text', 'World') 119 | ))).to.be.true; 120 | 121 | store.dispatch(addTodo('Top')); 122 | 123 | expect(equals(store.getState(), vector( 124 | hashMap('id', 3, 'text', 'Top'), 125 | hashMap('id', 1, 'text', 'Hello'), 126 | hashMap('id', 2, 'text', 'World') 127 | ))).to.be.true; 128 | 129 | store.replaceReducer(reducers.todos); 130 | 131 | store.dispatch(addTodo('Bottom')); 132 | 133 | expect(equals(store.getState(), vector( 134 | hashMap('id', 3, 'text', 'Top'), 135 | hashMap('id', 1, 'text', 'Hello'), 136 | hashMap('id', 2, 'text', 'World'), 137 | hashMap('id', 4, 'text', 'Bottom') 138 | ))).to.be.true; 139 | }); 140 | }); 141 | 142 | context('single subscription', () => { 143 | let store; 144 | 145 | beforeEach(() => { 146 | const rootReducer = combineReducers(reducers); 147 | store = createStore(rootReducer); 148 | }); 149 | 150 | it('should call listener correctly', () => { 151 | const listenerA = sinon.spy(() => {}); 152 | 153 | store.subscribe(listenerA); 154 | store.dispatch(unknownAction()); 155 | 156 | expect(listenerA).to.have.been.calledOnce; 157 | }); 158 | 159 | it('should unsubscribe single listener correctly', () => { 160 | const listenerA = sinon.spy(() => {}); 161 | const unsubscribeA = store.subscribe(listenerA); 162 | 163 | store.dispatch(unknownAction()); 164 | 165 | expect(listenerA).to.have.been.calledOnce; 166 | unsubscribeA(); 167 | 168 | store.dispatch(unknownAction()); 169 | expect(listenerA).to.have.been.calledOnce; 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /test/fixtures/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { hashMap } from 'mori'; 2 | import { ADD_TODO, UKNOWN_ACTION } from './actionTypes'; 3 | 4 | export const addTodo = (text) => 5 | hashMap('type', ADD_TODO, 'text', text); 6 | 7 | export const unknownAction = () => 8 | hashMap('type', UKNOWN_ACTION); 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TODO = 'ADD_TODO'; 2 | export const UKNOWN_ACTION = 'UNKNOWN_ACTION'; 3 | -------------------------------------------------------------------------------- /test/fixtures/reducers.js: -------------------------------------------------------------------------------- 1 | import { into, reduceKV, inc, hashMap, get, vector } from 'mori'; 2 | import { ADD_TODO } from './actionTypes'; 3 | 4 | const id = (state = vector()) => reduceKV(inc, 1, state); 5 | 6 | export function todos (state = vector(), action) { 7 | switch (get(action, 'type')) { 8 | case ADD_TODO: 9 | return into(state, vector(hashMap( 10 | 'id', id(state), 11 | 'text', get(action, 'text') 12 | ))); 13 | default: 14 | return state; 15 | } 16 | } 17 | 18 | export function todosReverse (state = [], action) { 19 | switch (get(action, 'type')) { 20 | case ADD_TODO: 21 | return into(vector(hashMap( 22 | 'id', id(state), 23 | 'text', get(action, 'text') 24 | )), state); 25 | default: 26 | return state; 27 | } 28 | } 29 | --------------------------------------------------------------------------------