├── .babelrc
├── .eslintrc
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── README.md
├── config
├── constants.js
├── karma.config.js
├── webpack.config.js
├── webpack.config.publish.js
└── webpack.config.test.js
├── package.json
├── script
└── build
└── src
├── index.js
├── index.spec.js
└── specs.context.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | env: {
3 | development: {
4 | plugins: [
5 | "typecheck"
6 | ]
7 | }
8 | },
9 | optional: [
10 | "es7.classProperties",
11 | "runtime"
12 | ],
13 | stage: 1
14 | }
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": [
4 | "standard",
5 | "standard-react"
6 | ],
7 | "env": {
8 | "mocha": true
9 | },
10 | "globals": {
11 | "sinon": true
12 | },
13 | "rules": {
14 | // overrides of the standard style
15 | "curly": [2, "all"],
16 | "max-len": [2, 100, 4],
17 | "semi": [2, "always"],
18 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
19 | "wrap-iife": [2, "outside"],
20 | // overrides of the standard-react style
21 | "react/jsx-sort-props": 2,
22 | "react/jsx-sort-prop-types": 2
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | cache:
4 | directories:
5 | - node_modules
6 | notifications:
7 | email: false
8 | node_js:
9 | - stable
10 | before_install:
11 | - npm i -g npm@^2.0.0
12 | before_script:
13 | - npm prune
14 | - export DISPLAY=:99.0
15 | - sh -e /etc/init.d/xvfb start
16 | after_success:
17 | - npm run semantic-release
18 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to this project
2 |
3 | The issue tracker is the preferred channel for [bug reports](#bugs),
4 | [features requests](#features) and [submitting pull
5 | requests](#pull-requests).
6 |
7 |
8 | ## Bug reports
9 |
10 | A bug is a _demonstrable problem_ that is caused by the code in the repository.
11 | Good bug reports are extremely helpful - thank you!
12 |
13 | Guidelines for bug reports:
14 |
15 | 1. **Use the GitHub issue search** — check if the issue has already been
16 | reported.
17 |
18 | 2. **Check if the issue has been fixed** — try to reproduce it using the
19 | latest `master` or development branch in the repository.
20 |
21 | 3. **Isolate the problem** — create a [reduced test
22 | case](http://css-tricks.com/reduced-test-cases/) and a live example.
23 |
24 | A good bug report contains as much detail as possible. What is your
25 | environment? What steps will reproduce the issue? What browser(s) and OS
26 | experience the problem? What would you expect to be the outcome? All these
27 | details really help!
28 |
29 | Example:
30 |
31 | > Short and descriptive example bug report title
32 | >
33 | > A summary of the issue and the browser/OS environment in which it occurs. If
34 | > suitable, include the steps required to reproduce the bug.
35 | >
36 | > 1. This is the first step
37 | > 2. This is the second step
38 | > 3. Further steps, etc.
39 | >
40 | > `` - a link to the reduced test case
41 | >
42 | > Any other information you want to share that is relevant to the issue being
43 | > reported. This might include the lines of code that you have identified as
44 | > causing the bug, and potential solutions (and your opinions on their
45 | > merits).
46 |
47 |
48 |
49 | ## Feature requests
50 |
51 | Feature requests are welcome. But take a moment to find out whether your idea
52 | fits with the scope and aims of the project. It's up to *you* to make a strong
53 | case to convince the project's developers of the merits of this feature. Please
54 | provide as much detail and context as possible.
55 |
56 |
57 |
58 | ## Pull requests
59 |
60 | Good pull requests - patches, improvements, new features - are a fantastic
61 | help. Please keep them focused in scope and avoid containing unrelated commits.
62 |
63 | **Please ask first** before embarking on any significant pull request (e.g.
64 | implementing new features or components, refactoring code), otherwise you risk
65 | spending a lot of time working on something that the project's developers might
66 | not want to merge into the project.
67 |
68 | Development commands:
69 |
70 | * `npm run build` – build the library
71 | * `npm run dev` – start the dev server and develop against live examples
72 | * `npm run lint` – run the linter
73 | * `npm run specs:watch` – run and watch the unit tests
74 |
75 | Please follow this process for submitting a patch:
76 |
77 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
78 | and configure the remotes:
79 |
80 | ```bash
81 | # Clone your fork of the repo into the current directory
82 | git clone https://github.com//react-native-web
83 | # Navigate to the newly cloned directory
84 | cd react-native-web
85 | # Assign the original repo to a remote called "upstream"
86 | git remote add upstream https://github.com/phuu/immutable-reducers
87 | ```
88 |
89 | 2. If you cloned a while ago, get the latest changes from upstream:
90 |
91 | ```bash
92 | git checkout master
93 | git pull upstream master
94 | ```
95 |
96 | 3. Create a new topic branch (off the main project development branch) to
97 | contain your feature, change, or fix:
98 |
99 | ```bash
100 | git checkout -b
101 | ```
102 |
103 | 4. Commit your changes in logical chunks. Please adhere to these [git commit
104 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
105 | or your code is unlikely be merged into the main project. Use Git's
106 | [interactive rebase](https://help.github.com/articles/interactive-rebase)
107 | feature to tidy up your commits before making them public.
108 |
109 | 5. Locally merge (or rebase) the upstream development branch into your topic branch:
110 |
111 | ```bash
112 | git pull [--rebase] upstream master
113 | ```
114 |
115 | 6. Push your topic branch up to your fork:
116 |
117 | ```bash
118 | git push origin
119 | ```
120 |
121 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
122 | with a clear title and description.
123 |
124 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to
125 | license your work under the same license as that used by the project.
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # immutable-reducers
2 |
3 | [](https://github.com/semantic-release/semantic-release) [](https://travis-ci.org/phuu/immutable-reducers)
4 |
5 | Create reducers for [immutable][immutable] data structures. Useful for [redux][redux].
6 |
7 | ## Table of contents
8 |
9 | * [Install](#install)
10 | * [Use](#use)
11 | * [`createReducer`](#createreducer)
12 | * [`combineReducers`](#combinereducers)
13 | * [Use](#use)
14 | * [Contributing](#contributing)
15 | * [Thanks](#thanks)
16 | * [License](#license)
17 |
18 | ## Install
19 |
20 | ```
21 | npm install --save immutable-reducers
22 | ```
23 |
24 | ## Use
25 |
26 | ```js
27 | import { fromJS } from 'immutable';
28 | import { combineReducers, createReducer } from 'immutable-reducers';
29 |
30 | // Setup some state (probably your app state)
31 |
32 | const initialState = fromJS({
33 | artist: {
34 | name: {
35 | first: 'Sean',
36 | last: 'Combs'
37 | },
38 | fans: 0
39 | }
40 | });
41 |
42 | // Create some reducers
43 |
44 | const artistNameReducer = createReducer(['artist', 'name'], (state, action) => {
45 | switch (action.type) {
46 | case 'RENAME':
47 | return fromJS(action.value);
48 | }
49 | return state;
50 | });
51 |
52 | // You can scope them by combining with an object
53 |
54 | const artistFansReducer = createReducer(['artis', 'fans'], (state, action) => {
55 | switch (action.type) {
56 | case 'NEW_FAN':
57 | return state + action.count;
58 | }
59 | return state;
60 | });
61 |
62 | // Combine 'em up
63 |
64 | const reducer = combineReducers(artistNameReducer, artistFansReducer);
65 |
66 | // Step the state
67 |
68 | reducer(initialState, { type: 'NEW_FAN', count: 1 });
69 |
70 | ```
71 |
72 | ## `createReducer`
73 |
74 | `createReducer` helps you make a reducer that operates on a small area of an immutable data structure.
75 |
76 | ```js
77 | createReducer(
78 | keyPath: Array,
79 | updater: (targetState: any, action: Object, state: any) => any
80 | ): any
81 | ```
82 |
83 | For example, given some initial state:
84 |
85 | ```js
86 | const initialState = fromJS({
87 | user: {
88 | favorites: Immutable.OrderedSet()
89 | }
90 | });
91 | ```
92 |
93 | We can create a reducer that looks out for favorite actions and remembers the id:
94 |
95 | ```js
96 | const favoriteReducer = createReducer(['user', 'favorites'], (favorites, action) => {
97 | switch (action.type) {
98 | case 'FAVORITE':
99 | return favorites.add(action.id);
100 | }
101 | return favorites;
102 | });
103 | ```
104 |
105 | `createReducer` handles reaching into the data structure and updating the value.
106 |
107 | Good to know:
108 |
109 | - If the updater function returns the same value it was called with, then no change will occur.
110 | - If the `keyPath` you specify does not exist, an Immutable `Map` will be created at each intermediary key.
111 | - The keys can be immutable data structures too #winning
112 | - The third argument to the reducer is the whole state object, allowing you consult other areas of state when handling an action
113 |
114 | ## `combineReducers`
115 |
116 | `combineReducers` is a less opinionated version of redux's default [`combineReducers`][redux-combinereducers] utility.
117 |
118 | ```js
119 | type Reducer = (state: any, action: Object) => any;
120 | type ReducerObject = Object;
121 |
122 | combineReducers(...Reducer|ReducerObject): Reducer
123 | ```
124 |
125 | It has two useful forms: applied to a list of reducers or to an object. When applied to the list, it acts like redux's combineReducers.
126 |
127 | When applied to an object, it's slightly different. `immutable-reducers` will use the key of the object to 'scope' the reducer further.
128 |
129 | For example, in the following example:
130 | You could scope it to the `user` key of your (immutable) state using `combineReducers`:
131 |
132 | ```js
133 | const combineReducer = combineReducers({
134 | user: createReducer(['favorites'], (favorites, action) => {
135 | switch (action.type) {
136 | case 'FAVORITE':
137 | return favorites.add(action.id);
138 | }
139 | return favorites;
140 | });
141 | });
142 | ```
143 |
144 | The end result is the same as the `createReducers` example, where the `favoritesReducer` will actually operate on the `['user', 'favorites']` key path.
145 |
146 | ## Contributing
147 |
148 | Please read the [contribution guidelines][contributing-url]. Contributions are
149 | welcome!
150 |
151 | ## Thanks
152 |
153 | Thanks to [rackt][rackt] for [redux][redux] and all those who work on [immutable][immutable].
154 |
155 | ## License
156 |
157 | Copyright (c) 2015 Tom Ashworth. Released under the [MIT
158 | license](http://www.opensource.org/licenses/mit-license.php).
159 |
160 | [contributing-url]: https://github.com/phuu/immutable-reducers/blob/master/CONTRIBUTING.md
161 | [immutable]: https://facebook.github.io/immutable-js/
162 | [rackt]: https://github.com/rackt
163 | [redux]: http://rackt.github.io/redux/
164 | [redux-combinereducers]: http://rackt.github.io/redux/docs/api/combineReducers.html
165 |
--------------------------------------------------------------------------------
/config/constants.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | var ROOT_DIRECTORY = path.resolve(__dirname, '..');
4 | var BUILD_DIRECTORY = path.resolve(ROOT_DIRECTORY, 'dist');
5 |
6 | module.exports = {
7 | ROOT_DIRECTORY: ROOT_DIRECTORY,
8 | BUILD_DIRECTORY: BUILD_DIRECTORY
9 | };
10 |
--------------------------------------------------------------------------------
/config/karma.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var constants = require('./constants');
4 | var webpackConfig = require('./webpack.config.test');
5 | // entry is determined by karma config 'files' array
6 | webpackConfig.entry = {};
7 |
8 | module.exports = function (config) {
9 | config.set({
10 | basePath: constants.ROOT_DIRECTORY,
11 | browsers: [ process.env.TRAVIS ? 'Firefox' : 'Chrome' ],
12 | browserNoActivityTimeout: 60000,
13 | client: {
14 | captureConsole: true,
15 | mocha: {
16 | ui: 'bdd'
17 | },
18 | useIframe: true
19 | },
20 | files: [
21 | 'src/specs.context.js'
22 | ],
23 | frameworks: [
24 | 'mocha'
25 | ],
26 | plugins: [
27 | 'karma-chrome-launcher',
28 | 'karma-firefox-launcher',
29 | 'karma-mocha',
30 | 'karma-sourcemap-loader',
31 | 'karma-webpack'
32 | ],
33 | preprocessors: {
34 | 'src/specs.context.js': [ 'webpack', 'sourcemap' ]
35 | },
36 | reporters: [ 'dots' ],
37 | singleRun: true,
38 | webpack: webpackConfig,
39 | webpackMiddleware: {
40 | stats: {
41 | assetsSort: 'name',
42 | colors: true,
43 | children: false,
44 | chunks: false,
45 | modules: false
46 | }
47 | }
48 | });
49 | };
50 |
--------------------------------------------------------------------------------
/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | var DedupePlugin = webpack.optimize.DedupePlugin;
4 | var OccurenceOrderPlugin = webpack.optimize.OccurenceOrderPlugin;
5 | var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
6 |
7 | var plugins = [
8 | new DedupePlugin(),
9 | new OccurenceOrderPlugin()
10 | ];
11 |
12 | if (process.env.NODE_ENV === 'production') {
13 | plugins.push(
14 | new UglifyJsPlugin({
15 | compress: {
16 | dead_code: true,
17 | drop_console: true,
18 | screw_ie8: true,
19 | warnings: true
20 | }
21 | })
22 | );
23 | }
24 |
25 | module.exports = {
26 | entry: './src',
27 | module: {
28 | loaders: [
29 | {
30 | test: /\.jsx?$/,
31 | exclude: /node_modules/,
32 | loader: 'babel-loader'
33 | }
34 | ]
35 | },
36 | output: {
37 | path: './dist',
38 | filename: 'immutable-reducers.js'
39 | },
40 | resolve: {
41 | extensions: [
42 | '',
43 | '.js',
44 | '.jsx'
45 | ]
46 | },
47 | plugins: plugins
48 | };
49 |
--------------------------------------------------------------------------------
/config/webpack.config.publish.js:
--------------------------------------------------------------------------------
1 | var assign = require('object-assign');
2 | var constants = require('./constants');
3 | var baseConfig = require('./webpack.config');
4 |
5 | module.exports = assign(baseConfig, {
6 | output: {
7 | filename: 'immutable-reducers.js',
8 | library: 'ImmutableReducers',
9 | libraryTarget: 'commonjs2',
10 | path: constants.BUILD_DIRECTORY
11 | }
12 | });
13 |
--------------------------------------------------------------------------------
/config/webpack.config.test.js:
--------------------------------------------------------------------------------
1 | var assign = require('object-assign');
2 | var baseConfig = require('./webpack.config');
3 |
4 | module.exports = assign(baseConfig, {
5 | // fixes sourcemaps in karma
6 | devtool: 'inline-source-map',
7 | // builds are faster when css is excluded
8 | features: {
9 | removeStylesheet: true
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "immutable-reducers",
3 | "description": "Create reducers for immutable data structures. Useful for redux.",
4 | "scripts": {
5 | "build": "rm -rf ./dist && NODE_ENV=publish webpack --config config/webpack.config.publish.js --sort-assets-by --progress",
6 | "lint": "eslint config src",
7 | "prepublish": "npm run build",
8 | "specs": "NODE_ENV=test karma start config/karma.config.js",
9 | "specs:watch": "npm run specs -- --no-single-run",
10 | "test": "npm run specs && npm run lint",
11 | "semantic-release": "semantic-release pre && npm publish && semantic-release post"
12 | },
13 | "main": "dist/immutable-reducers.js",
14 | "files": [
15 | "dist"
16 | ],
17 | "author": "Tom Ashworth ",
18 | "license": "MIT",
19 | "devDependencies": {
20 | "babel-core": "^5.8.24",
21 | "babel-eslint": "^4.1.1",
22 | "babel-loader": "^5.3.2",
23 | "babel-plugin-typecheck": "^3.6.1",
24 | "babel-runtime": "^5.8.20",
25 | "chai": "^3.2.0",
26 | "eslint": "^1.3.1",
27 | "eslint-config-standard": "^4.3.1",
28 | "eslint-config-standard-react": "^2.0.0",
29 | "eslint-plugin-react": "^3.3.1",
30 | "eslint-plugin-standard": "^1.3.0",
31 | "immutable": "^3.7.5",
32 | "karma": "^0.13.9",
33 | "karma-chrome-launcher": "^0.2.0",
34 | "karma-cli": "^0.1.0",
35 | "karma-firefox-launcher": "^0.1.6",
36 | "karma-mocha": "^0.2.0",
37 | "karma-sourcemap-loader": "^0.3.5",
38 | "karma-webpack": "^1.7.0",
39 | "mocha": "^2.3.2",
40 | "object-assign": "^4.0.1",
41 | "webpack": "^1.12.1",
42 | "semantic-release": "^4.3.4"
43 | },
44 | "dependencies": {},
45 | "repository": {
46 | "type": "git",
47 | "url": "https://github.com/phuu/immutable-reducers.git"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/script/build:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Generate build artifacts
4 |
5 | webpack --config config/webpack.config.js
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Combine multiple reducers into a single, with optional scoping
3 | *
4 | * Example:
5 |
6 | combineReducers(userReducer, followersReducer)
7 |
8 | // Scoped to key 10
9 | item10Reducer = combineReducers({
10 | 10: itemReducer
11 | })
12 |
13 | // Compose 'em up
14 | basketItem10Reducer = combineReducers({
15 | basket: item10Reducer
16 | })
17 |
18 | * Returns a reducing function.
19 | */
20 | const combineReducers = (...rawReducers) => {
21 | const reducers = rawReducers.reduce((rs, reducer) => {
22 | // Keep the plain 'ol functions
23 | if (typeof reducer === 'function') {
24 | return rs.concat(reducer);
25 | }
26 |
27 | // Scope the reducers by their keys, if keyed they are
28 | if (typeof reducer === 'object' && !Array.isArray(reducer)) {
29 | return Object.keys(reducer).reduce((rs, k) => {
30 | // Value is a function, so we can make it into a reducer!
31 | if (typeof reducer[k] === 'function') {
32 | return rs.concat(
33 | createReducer([k], reducer[k])
34 | );
35 | }
36 |
37 | // Ignore it
38 | return rs;
39 | }, rs);
40 | }
41 |
42 | // Otherwise, ignore this one
43 | return rs;
44 | }, []);
45 |
46 | // Return a function that iterates over the reducers and reduces from some initial state,
47 | // with an action as input. Easy now.
48 | return (initialState, action) =>
49 | reducers.reduce(
50 | (state, reducer) => reducer(state, action),
51 | initialState
52 | );
53 | };
54 |
55 | /**
56 | * Create a reducer scoped to the specified keys.
57 | *
58 | * Example:
59 |
60 | createReducer(['user', 'followers'], (state, action) => {
61 | if (action.type === 'FOLLOW') {
62 | return state + 1;
63 | }
64 | return state;
65 | })
66 |
67 | * Returns a reducing function.
68 | */
69 | const createReducer = (path, reducer) => (initialState, action, globalState = initialState) =>
70 | initialState.updateIn(path, v => reducer(v, action, globalState));
71 |
72 | export default {
73 | combineReducers,
74 | createReducer
75 | };
76 |
--------------------------------------------------------------------------------
/src/index.spec.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import { fromJS } from 'immutable';
3 |
4 | import { combineReducers, createReducer } from '.';
5 |
6 | const initialState = fromJS({
7 | artist: {
8 | name: {
9 | first: 'Sean',
10 | last: 'Combs'
11 | },
12 | followers: 0
13 | },
14 | albums: [
15 | { name: 'No Way Out', year: '1997' },
16 | { name: 'Forever', year: '1999' }
17 | ],
18 | related: {
19 | artist: {
20 | name: {
21 | first: 'Mary J.',
22 | last: 'Blige'
23 | },
24 | followers: 0
25 | }
26 | }
27 | });
28 |
29 | const nameReducer = createReducer(['artist', 'name'], (name, action) => {
30 | switch (action.type) {
31 | case 'RENAME':
32 | return fromJS(action.value);
33 | }
34 | return name;
35 | });
36 |
37 | const followersReducer = createReducer(['artist', 'followers'], (followers, action) => {
38 | switch (action.type) {
39 | case 'FOLLOW':
40 | return followers + action.value;
41 | }
42 | return followers;
43 | });
44 |
45 | const albumsReducer = createReducer(['albums'], (albums, action) => {
46 | switch (action.type) {
47 | case 'RELEASE':
48 | return albums.push(fromJS(action.value));
49 | }
50 | return albums;
51 | });
52 |
53 | const fakePathReducer = createReducer(['this', 'path', 'is', 'fake'], (_, action) => {
54 | return fromJS({ no: 'thanks' });
55 | });
56 |
57 | const actions = {
58 | FOLLOW: { type: 'FOLLOW', value: 1 },
59 | RENAME: { type: 'RENAME', value: { first: 'Puff', last: 'Daddy' } },
60 | NOOP: { type: 'NOOP', value: {} },
61 | RELEASE: { type: 'RELEASE', value: { name: 'The Saga Continues...', year: '2001' } }
62 | };
63 |
64 | describe('createReducer', () => {
65 | it('creates reducers than can reach into data structures', () => {
66 | assert(
67 | followersReducer(initialState, actions.FOLLOW).getIn(['artist', 'followers']) === 1,
68 | 'followers state is incremented by 1'
69 | );
70 | });
71 |
72 | it('does not modify the data if nothing changes', () => {
73 | assert(
74 | nameReducer(initialState, actions.NOOP) === initialState,
75 | 'data is untouched'
76 | );
77 | });
78 |
79 | it('can handle non-existant paths', () => {
80 | assert(
81 | fakePathReducer(initialState, actions.NOOP).hasIn(['this', 'path', 'is', 'fake']),
82 | 'non-existant path is created'
83 | );
84 | });
85 |
86 | it('passes whole state object', () => {
87 | const reducer = createReducer(['artist'], (artist, action, state) => {
88 | assert(
89 | state === initialState,
90 | 'is passed initialState'
91 | );
92 | assert(
93 | artist === state.get('artist'),
94 | 'gets target state object'
95 | );
96 | });
97 |
98 | reducer(initialState, {});
99 | });
100 |
101 | it('the state object is passed through composition', () => {
102 | const nameReducer = createReducer(['name'], (name, action, state) => {
103 | assert(
104 | state === initialState,
105 | 'is passed initialState'
106 | );
107 | assert(
108 | name === initialState.getIn(['artist', 'name']),
109 | 'gets target state object'
110 | );
111 | return name;
112 | });
113 |
114 | const reducer = combineReducers({
115 | artist: nameReducer
116 | });
117 |
118 | reducer(initialState, {});
119 | });
120 | });
121 |
122 | describe('combineReducers', () => {
123 | it('can take an object to scope reducer', () => {
124 | const combinedReducer = combineReducers({
125 | related: followersReducer
126 | });
127 | const result = combinedReducer(initialState, actions.FOLLOW);
128 |
129 | assert(
130 | result.getIn(['related', 'artist', 'followers']) === 1,
131 | 'scoped data is updated'
132 | );
133 | });
134 |
135 | it('can combine reducers in a sequence', () => {
136 | const combinedReducer = combineReducers(nameReducer, albumsReducer);
137 | const result = combinedReducer(initialState, actions.RELEASE);
138 |
139 | assert(
140 | result.get('albums').count() === 3,
141 | 'albums is updated'
142 | );
143 | assert(
144 | result.get('artist') === initialState.get('artist'),
145 | 'artist is untouched'
146 | );
147 | });
148 |
149 | it('can combine combined reducers', () => {
150 | const artistReducer = combineReducers(nameReducer, followersReducer);
151 | const combinedReducer = combineReducers(artistReducer, albumsReducer);
152 | const result = combinedReducer(initialState, actions.RENAME);
153 |
154 | assert(
155 | result.get('albums').count() === 2,
156 | 'albums is updated'
157 | );
158 | assert(
159 | result.get('artist') !== initialState.get('artist'),
160 | 'artist is touched'
161 | );
162 | });
163 | });
164 |
--------------------------------------------------------------------------------
/src/specs.context.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Since we use webpack-specific features in our modules (e.g., loaders,
3 | * plugins, adding CSS to the dependency graph), we must use webpack to build a
4 | * test bundle.
5 | *
6 | * This module creates a context of all the unit test files (as per the unit
7 | * test naming convention). It's used as the webpack entry file for unit tests.
8 | *
9 | * See: https://github.com/webpack/docs/wiki/context
10 | */
11 |
12 | const specsContext = require.context('.', true, /.+\.spec\.js$/);
13 | specsContext.keys().forEach(specsContext);
14 | module.exports = specsContext;
15 |
--------------------------------------------------------------------------------