├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gulpfile.js ├── package.json ├── src ├── actions.js ├── constants.js ├── index.js └── reducers │ ├── card-stack.js │ ├── helpers.js │ └── tab-reducer.js └── test ├── .eslintrc ├── runner.html ├── setup ├── .globals.json ├── browser.js ├── node.js └── setup.js └── unit ├── actions.js ├── constants.js ├── index.js └── reducers.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: true 4 | 5 | ratings: 6 | paths: 7 | - "src/**/**.js" -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true; 4 | 5 | [*] 6 | # Ensure there's no lingering whitespace 7 | trim_trailing_whitespace = true 8 | # Ensure a newline at the end of each file 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | # Unix-style newlines 13 | end_of_line = lf 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | }, 6 | "rules": { 7 | # Possible Errors 8 | comma-dangle: [2, never], 9 | no-cond-assign: 2, 10 | no-console: 0, 11 | no-constant-condition: 2, 12 | no-control-regex: 2, 13 | no-debugger: 2, 14 | no-dupe-args: 2, 15 | no-dupe-keys: 2, 16 | no-duplicate-case: 2, 17 | no-empty: 2, 18 | no-empty-character-class: 2, 19 | no-ex-assign: 2, 20 | no-extra-boolean-cast: 2, 21 | no-extra-parens: 0, 22 | no-extra-semi: 2, 23 | no-func-assign: 2, 24 | no-inner-declarations: [2, functions], 25 | no-invalid-regexp: 2, 26 | no-irregular-whitespace: 2, 27 | no-negated-in-lhs: 2, 28 | no-obj-calls: 2, 29 | no-regex-spaces: 2, 30 | no-sparse-arrays: 2, 31 | no-unexpected-multiline: 2, 32 | no-unreachable: 2, 33 | use-isnan: 2, 34 | valid-jsdoc: 0, 35 | valid-typeof: 2, 36 | 37 | # Best Practices 38 | accessor-pairs: 2, 39 | block-scoped-var: 0, 40 | complexity: [2, 6], 41 | 42 | consistent-return: 0, 43 | curly: 0, 44 | default-case: 0, 45 | dot-location: 0, 46 | dot-notation: 0, 47 | eqeqeq: 2, 48 | guard-for-in: 2, 49 | no-alert: 2, 50 | no-caller: 2, 51 | no-case-declarations: 2, 52 | no-div-regex: 2, 53 | no-else-return: 0, 54 | no-empty-pattern: 2, 55 | no-eq-null: 2, 56 | no-eval: 2, 57 | no-extend-native: 2, 58 | no-extra-bind: 2, 59 | no-fallthrough: 2, 60 | no-floating-decimal: 0, 61 | no-implicit-coercion: 0, 62 | no-implied-eval: 2, 63 | no-invalid-this: 0, 64 | no-iterator: 2, 65 | no-labels: 0, 66 | no-lone-blocks: 2, 67 | no-loop-func: 2, 68 | no-magic-number: 0, 69 | no-multi-spaces: 0, 70 | no-multi-str: 0, 71 | no-native-reassign: 2, 72 | no-new-func: 2, 73 | no-new-wrappers: 2, 74 | no-new: 2, 75 | no-octal-escape: 2, 76 | no-octal: 2, 77 | no-proto: 2, 78 | no-redeclare: 2, 79 | no-return-assign: 2, 80 | no-script-url: 2, 81 | no-self-compare: 2, 82 | no-sequences: 0, 83 | no-throw-literal: 0, 84 | no-unused-expressions: 2, 85 | no-useless-call: 2, 86 | no-useless-concat: 2, 87 | no-void: 2, 88 | no-warning-comments: 0, 89 | no-with: 2, 90 | radix: 2, 91 | vars-on-top: 0, 92 | wrap-iife: 2, 93 | yoda: 0, 94 | 95 | # Strict 96 | strict: 0, 97 | 98 | # Variables 99 | init-declarations: 0, 100 | no-catch-shadow: 2, 101 | no-delete-var: 2, 102 | no-label-var: 2, 103 | no-shadow-restricted-names: 2, 104 | no-shadow: 0, 105 | no-undef-init: 2, 106 | no-undef: 0, 107 | no-undefined: 0, 108 | no-unused-vars: 0, 109 | no-use-before-define: 0, 110 | 111 | # Node.js and CommonJS 112 | callback-return: 2, 113 | handle-callback-err: 2, 114 | no-mixed-requires: 0, 115 | no-new-require: 0, 116 | no-path-concat: 2, 117 | no-process-exit: 2, 118 | no-restricted-modules: 0, 119 | no-sync: 0, 120 | 121 | # Stylistic Issues 122 | array-bracket-spacing: 0, 123 | block-spacing: 0, 124 | brace-style: 0, 125 | camelcase: 0, 126 | comma-spacing: 0, 127 | comma-style: 0, 128 | computed-property-spacing: 0, 129 | consistent-this: 0, 130 | eol-last: 0, 131 | func-names: 0, 132 | func-style: 0, 133 | id-length: 0, 134 | id-match: 0, 135 | indent: 0, 136 | jsx-quotes: 0, 137 | key-spacing: 0, 138 | linebreak-style: 0, 139 | lines-around-comment: 0, 140 | max-depth: 0, 141 | max-len: 0, 142 | max-nested-callbacks: 0, 143 | max-params: 0, 144 | max-statements: [2, 30], 145 | new-cap: 0, 146 | new-parens: 0, 147 | newline-after-var: 0, 148 | no-array-constructor: 0, 149 | no-bitwise: 0, 150 | no-continue: 0, 151 | no-inline-comments: 0, 152 | no-lonely-if: 0, 153 | no-mixed-spaces-and-tabs: 0, 154 | no-multiple-empty-lines: 0, 155 | no-negated-condition: 0, 156 | no-nested-ternary: 0, 157 | no-new-object: 0, 158 | no-plusplus: 0, 159 | no-restricted-syntax: 0, 160 | no-spaced-func: 0, 161 | no-ternary: 0, 162 | no-trailing-spaces: 0, 163 | no-underscore-dangle: 0, 164 | no-unneeded-ternary: 0, 165 | object-curly-spacing: 0, 166 | one-var: 0, 167 | operator-assignment: 0, 168 | operator-linebreak: 0, 169 | padded-blocks: 0, 170 | quote-props: 0, 171 | quotes: 0, 172 | require-jsdoc: 0, 173 | semi-spacing: 0, 174 | semi: 0, 175 | sort-vars: 0, 176 | space-after-keywords: 0, 177 | space-before-blocks: 0, 178 | space-before-function-paren: 0, 179 | space-before-keywords: 0, 180 | space-in-parens: 0, 181 | space-infix-ops: 0, 182 | space-return-throw-case: 0, 183 | space-unary-ops: 0, 184 | spaced-comment: 0, 185 | wrap-regex: 0, 186 | 187 | # ECMAScript 6 188 | arrow-body-style: 0, 189 | arrow-parens: 0, 190 | arrow-spacing: 0, 191 | constructor-super: 0, 192 | generator-star-spacing: 0, 193 | no-arrow-condition: 0, 194 | no-class-assign: 0, 195 | no-const-assign: 0, 196 | no-dupe-class-members: 0, 197 | no-this-before-super: 0, 198 | no-var: 0, 199 | object-shorthand: 0, 200 | prefer-arrow-callback: 0, 201 | prefer-const: 0, 202 | prefer-reflect: 0, 203 | prefer-spread: 0, 204 | prefer-template: 0, 205 | require-yield: 0 206 | }, 207 | "env": { 208 | "browser": true, 209 | "node": true 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | dist 4 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | bower_components 27 | coverage 28 | tmp 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | 33 | # Source code 34 | src 35 | 36 | # babelrc (otherwise react native gets confused somehow) 37 | .babelrc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "stable" 6 | sudo: false 7 | script: "gulp coverage" 8 | after_success: 9 | - npm install -g codeclimate-test-reporter 10 | - codeclimate-test-reporter < coverage/lcov.info 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### [0.0.1](https://github.com/bakeryhq/react-native-navigation-redux-helpers/releases/tag/v0.0.1) 2 | 3 | - The first release 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 hi@thebakery.io 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 | # React Native Navigation Redux helpers 2 | [![Build Status](https://travis-ci.org/bakery/react-native-navigation-redux-helpers.svg?branch=master)](https://travis-ci.org/bakery/react-native-navigation-redux-helpers) 3 | [![Code Climate](https://codeclimate.com/github/thebakeryio/react-native-navigation-redux-helpers/badges/gpa.svg)](https://codeclimate.com/github/thebakeryio/react-native-navigation-redux-helpers) 4 | [![Dependency Status](https://david-dm.org/thebakeryio/react-native-navigation-redux-helpers.svg)](https://david-dm.org/thebakeryio/react-native-navigation-redux-helpers) 5 | [![devDependency Status](https://david-dm.org/thebakeryio/react-native-navigation-redux-helpers/dev-status.svg)](https://david-dm.org/thebakeryio/react-native-navigation-redux-helpers#info=devDependencies) 6 | 7 | Reducers and actions to implement navigation in React Native applications (RN 0.28.0+) 8 | 9 | ## When to use this 10 | 11 | - you are using RN ExperimentalNavigation 12 | - you are using Redux 13 | - you do not want to write and re-write your own actions and reducers for navigation 14 | 15 | ## Getting started 16 | 17 | ```bash 18 | npm install --save react-native-navigation-redux-helpers 19 | ``` 20 | 21 | ### Card navigation 22 | 23 | Define your card reducer 24 | 25 | ```javascript 26 | import { cardStackReducer } from 'react-native-navigation-redux-helpers'; 27 | 28 | const initialState = { 29 | key: 'global', 30 | index: 0, 31 | routes: [ 32 | { 33 | key: 'applicationSection1', 34 | index: 0 35 | }, 36 | ], 37 | }; 38 | 39 | module.exports = cardStackReducer(initialState); 40 | ``` 41 | 42 | Use this reducer in NavigationCardStack in your component 43 | 44 | ```javascript 45 | import { NavigationExperimental } from 'react-native'; 46 | import React, { Component } from 'react'; 47 | import { connect } from 'react-redux'; 48 | import { actions } from 'react-native-navigation-redux-helpers'; 49 | 50 | const { 51 | popRoute, 52 | pushRoute, 53 | } = actions; 54 | 55 | const { 56 | CardStack: NavigationCardStack 57 | } = NavigationExperimental; 58 | 59 | class GlobalNavigation extends Component { 60 | render() { 61 | return ( 62 | 67 | ); 68 | } 69 | 70 | /* ... */ 71 | 72 | onGoBack() { 73 | const { dispatch, navigation } = this.props; 74 | dispatch(popRoute(navigation.key)); 75 | } 76 | 77 | onGoSomewhere() { 78 | const { dispatch, navigation } = this.props; 79 | dispatch(pushRoute({ key: 'sowhere else' }, navigation.key)); 80 | } 81 | } 82 | 83 | function mapDispatchToProps(dispatch) { 84 | return { 85 | dispatch 86 | }; 87 | } 88 | 89 | function mapStateToProps(state) { 90 | return { 91 | // XX: assuming you've registered the reducer above under the name 'cardNavigation' 92 | navigation: state.cardNavigation 93 | }; 94 | } 95 | 96 | export default connect(mapStateToProps, mapDispatchToProps)(GlobalNavigation); 97 | 98 | ``` 99 | 100 | ### Tab navigation 101 | 102 | Define your tab reducer 103 | 104 | ```javascript 105 | import { tabReducer } from 'react-native-navigation-redux-helpers'; 106 | 107 | const tabs = { 108 | routes: [ 109 | { key: 'feed', title: 'Items' }, 110 | { key: 'notifications', title: 'Notifications' }, 111 | { key: 'settings', title: 'Settings' } 112 | ], 113 | key: 'ApplicationTabs', 114 | index: 0 115 | }; 116 | 117 | module.exports = tabReducer(tabs); 118 | ``` 119 | 120 | And now put it to good use inside your component 121 | 122 | ```javascript 123 | import { TabBarIOS } from 'react-native'; 124 | import React, { Component } from 'react'; 125 | import Feed from '../Feed'; 126 | import { connect } from 'react-redux'; 127 | import { actions as navigationActions } from 'react-native-navigation-redux-helpers'; 128 | 129 | const { jumpTo } = navigationActions; 130 | 131 | class ApplicationTabs extends Component { 132 | _renderTabContent(tab) { 133 | if (tab.key === 'feed') { 134 | return ( 135 | 136 | ); 137 | } 138 | 139 | /* ... */ 140 | } 141 | 142 | render() { 143 | const { dispatch, navigation } = this.props; 144 | const children = navigation.routes.map( (tab, i) => { 145 | return ( 146 | dispatch(jumpTo(i, navigation.key)) } 150 | selected={this.props.navigation.index === i}> 151 | { this._renderTabContent(tab) } 152 | 153 | ); 154 | }); 155 | return ( 156 | 157 | {children} 158 | 159 | ); 160 | } 161 | } 162 | 163 | function mapDispatchToProps(dispatch) { 164 | return { 165 | dispatch 166 | }; 167 | } 168 | 169 | function mapStateToProps(state) { 170 | return { 171 | // XX: assuming your tab reducer is registered as 'tabs' 172 | navigation: state.tabs 173 | }; 174 | } 175 | export default connect(mapStateToProps, mapDispatchToProps)(ApplicationTabs); 176 | ``` 177 | 178 | ## Supported actions 179 | 180 | ### cardStackReducer 181 | 182 | - pushRoute 183 | - popRoute 184 | - jumpTo 185 | - reset 186 | - replaceAt 187 | - replaceAtIndex 188 | - jumpToIndex 189 | - back 190 | - forward 191 | 192 | ### tabReducer 193 | 194 | - jumpTo 195 | - jumpToIndex 196 | 197 | ## Complete examples 198 | 199 | - [Example using RN experimental navigation with Redux](https://github.com/thebakeryio/react-native-complex-nav) 200 | - [TodoMVC React Native](https://github.com/thebakeryio/todomvc-react-native) 201 | 202 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const loadPlugins = require('gulp-load-plugins'); 3 | const del = require('del'); 4 | const glob = require('glob'); 5 | const path = require('path'); 6 | const isparta = require('isparta'); 7 | const webpack = require('webpack'); 8 | const webpackStream = require('webpack-stream'); 9 | const source = require('vinyl-source-stream'); 10 | 11 | const Instrumenter = isparta.Instrumenter; 12 | const mochaGlobals = require('./test/setup/.globals'); 13 | const manifest = require('./package.json'); 14 | 15 | // Load all of our Gulp plugins 16 | const $ = loadPlugins(); 17 | 18 | // Gather the library data from `package.json` 19 | const config = manifest.babelBoilerplateOptions; 20 | const mainFile = manifest.main; 21 | const destinationFolder = path.dirname(mainFile); 22 | const exportFileName = path.basename(mainFile, path.extname(mainFile)); 23 | 24 | function cleanDist(done) { 25 | del([destinationFolder]).then(() => done()); 26 | } 27 | 28 | function cleanTmp(done) { 29 | del(['tmp']).then(() => done()); 30 | } 31 | 32 | // Lint a set of files 33 | function lint(files) { 34 | return gulp.src(files) 35 | .pipe($.eslint()) 36 | .pipe($.eslint.format()) 37 | .pipe($.eslint.failAfterError()); 38 | } 39 | 40 | function lintSrc() { 41 | return lint('src/**/*.js'); 42 | } 43 | 44 | function lintTest() { 45 | return lint('test/**/*.js'); 46 | } 47 | 48 | function lintGulpfile() { 49 | return lint('gulpfile.js'); 50 | } 51 | 52 | function build() { 53 | return gulp.src(path.join('src', config.entryFileName)) 54 | .pipe(webpackStream({ 55 | output: { 56 | filename: exportFileName + '.js', 57 | libraryTarget: 'umd', 58 | library: config.mainVarName 59 | }, 60 | externals: [ 61 | { 62 | 'react-native': true 63 | } 64 | ], 65 | module: { 66 | loaders: [ 67 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } 68 | ] 69 | }, 70 | devtool: 'source-map' 71 | })) 72 | .pipe(gulp.dest(destinationFolder)) 73 | .pipe($.filter(['**', '!**/*.js.map'])) 74 | .pipe($.rename(exportFileName + '.min.js')) 75 | .pipe($.sourcemaps.init({ loadMaps: true })) 76 | .pipe($.uglify()) 77 | .pipe($.sourcemaps.write('./')) 78 | .pipe(gulp.dest(destinationFolder)); 79 | } 80 | 81 | function _mocha() { 82 | return gulp.src(['test/setup/node.js', 'test/unit/**/*.js'], {read: false}) 83 | .pipe($.mocha({ 84 | reporter: 'dot', 85 | globals: Object.keys(mochaGlobals.globals), 86 | ignoreLeaks: false 87 | })); 88 | } 89 | 90 | function _registerBabel() { 91 | // eslint-disable-line global-require 92 | require('babel-register'); 93 | } 94 | 95 | function _registerTestBabel() { 96 | require('babel-register')({ 97 | plugins: ['babel-plugin-rewire'] 98 | }); 99 | } 100 | 101 | function test() { 102 | _registerTestBabel(); 103 | return _mocha(); 104 | } 105 | 106 | function coverage(done) { 107 | gulp.src(['src/**/*.js']) 108 | .pipe($.babel({ 109 | plugins: ['babel-plugin-rewire'] 110 | })) 111 | .pipe($.istanbul({ instrumenter: Instrumenter })) 112 | .pipe($.istanbul.hookRequire()) 113 | .on('finish', () => { 114 | return test() 115 | .pipe($.istanbul.writeReports()) 116 | .on('end', done); 117 | }); 118 | } 119 | 120 | const watchFiles = ['src/**/*', 'test/**/*', 'package.json', '**/.eslintrc', '.jscsrc']; 121 | 122 | // Run the headless unit tests as you make changes. 123 | function watch() { 124 | gulp.watch(watchFiles, ['test']); 125 | } 126 | 127 | function testBrowser() { 128 | // Our testing bundle is made up of our unit tests, which 129 | // should individually load up pieces of our application. 130 | // We also include the browser setup file. 131 | const testFiles = glob.sync('./test/unit/**/*.js'); 132 | const allFiles = ['./test/setup/browser.js'].concat(testFiles); 133 | 134 | // Lets us differentiate between the first build and subsequent builds 135 | var firstBuild = true; 136 | 137 | // This empty stream might seem like a hack, but we need to specify all of our files through 138 | // the `entry` option of webpack. Otherwise, it ignores whatever file(s) are placed in here. 139 | return gulp.src('') 140 | .pipe($.plumber()) 141 | .pipe(webpackStream({ 142 | watch: true, 143 | entry: allFiles, 144 | output: { 145 | filename: '__spec-build.js' 146 | }, 147 | // Externals isn't necessary here since these are for tests. 148 | module: { 149 | loaders: [ 150 | // This is what allows us to author in future JavaScript 151 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?plugins=babel-plugin-rewire' }, 152 | // This allows the test setup scripts to load `package.json` 153 | { test: /\.json$/, exclude: /node_modules/, loader: 'json-loader' } 154 | ] 155 | }, 156 | plugins: [ 157 | // By default, webpack does `n=>n` compilation with entry files. This concatenates 158 | // them into a single chunk. 159 | new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }) 160 | ], 161 | devtool: 'inline-source-map' 162 | }, null, function() { 163 | if (firstBuild) { 164 | $.livereload.listen({port: 35729, host: 'localhost', start: true}); 165 | var watcher = gulp.watch(watchFiles, ['lint']); 166 | } else { 167 | $.livereload.reload('./tmp/__spec-build.js'); 168 | } 169 | firstBuild = false; 170 | })) 171 | .pipe(gulp.dest('./tmp')); 172 | } 173 | 174 | // Remove the built files 175 | gulp.task('clean', cleanDist); 176 | 177 | // Remove our temporary files 178 | gulp.task('clean-tmp', cleanTmp); 179 | 180 | // Lint our source code 181 | gulp.task('lint-src', lintSrc); 182 | 183 | // Lint our test code 184 | gulp.task('lint-test', lintTest); 185 | 186 | // Lint this file 187 | gulp.task('lint-gulpfile', lintGulpfile); 188 | 189 | // Lint everything 190 | gulp.task('lint', ['lint-src', 'lint-test', 'lint-gulpfile']); 191 | 192 | // Build two versions of the library 193 | gulp.task('build', ['lint', 'clean'], build); 194 | 195 | // Lint and run our tests 196 | gulp.task('test', ['lint'], test); 197 | 198 | // Set up coverage and run tests 199 | gulp.task('coverage', ['lint'], coverage); 200 | 201 | // Set up a livereload environment for our spec runner `test/runner.html` 202 | gulp.task('test-browser', ['lint', 'clean-tmp'], testBrowser); 203 | 204 | // Run the headless unit tests as you make changes. 205 | gulp.task('watch', watch); 206 | 207 | // An alias of test 208 | gulp.task('default', ['test']); 209 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-navigation-redux-helpers", 3 | "version": "0.5.0", 4 | "description": "Redux helpers for React Native navigation", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "gulp", 8 | "lint": "gulp lint", 9 | "test-browser": "gulp test-browser", 10 | "watch": "gulp watch", 11 | "build": "gulp build", 12 | "coverage": "gulp coverage", 13 | "prepublish": "npm run lint && npm test && npm run build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/thebakeryio/react-native-navigation-redux-helpers.git" 18 | }, 19 | "keywords": [], 20 | "author": "The Bakery", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/thebakeryio/react-native-navigation-redux-helpers/issues" 24 | }, 25 | "homepage": "https://github.com/thebakeryio/react-native-navigation-redux-helpers", 26 | "devDependencies": { 27 | "babel-core": "^6.3.26", 28 | "babel-loader": "^6.2.0", 29 | "babel-plugin-rewire": "^1.0.0-rc-4", 30 | "babel-polyfill": "^6.3.14", 31 | "babel-preset-es2015": "^6.3.13", 32 | "babel-register": "^6.3.13", 33 | "chai": "^3.4.1", 34 | "del": "^2.2.0", 35 | "glob": "^7.0.3", 36 | "gulp": "^3.9.0", 37 | "gulp-babel": "^6.1.2", 38 | "gulp-eslint": "^3.0.1", 39 | "gulp-filter": "^4.0.0", 40 | "gulp-istanbul": "^1.1.1", 41 | "gulp-livereload": "^3.8.1", 42 | "gulp-load-plugins": "^1.1.0", 43 | "gulp-mocha": "^3.0.1", 44 | "gulp-plumber": "^1.0.1", 45 | "gulp-rename": "^1.2.2", 46 | "gulp-sourcemaps": "^2.1.1", 47 | "gulp-uglify": "^2.0.0", 48 | "isparta": "^4.0.0", 49 | "json-loader": "^0.5.3", 50 | "mocha": "^3.0.2", 51 | "sinon": "^1.17.2", 52 | "sinon-chai": "^2.8.0", 53 | "vinyl-source-stream": "^1.1.0", 54 | "webpack": "^1.12.9", 55 | "webpack-stream": "^3.1.0" 56 | }, 57 | "peerDependencies": { 58 | "react-native": ">=0.28.0" 59 | }, 60 | "babelBoilerplateOptions": { 61 | "entryFileName": "index.js" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | PUSH_ROUTE, 3 | POP_ROUTE, 4 | RESET_ROUTE, 5 | REPLACE_AT, 6 | REPLACE_AT_INDEX, 7 | JUMP_TO, 8 | JUMP_TO_INDEX, 9 | BACK, 10 | FORWARD, 11 | GET, 12 | HAS, 13 | INDEX_OF 14 | } from './constants'; 15 | 16 | export function pushRoute(route, key) { 17 | if (!key) { 18 | throw new Error('pushRoute requires key argument'); 19 | } 20 | 21 | return { 22 | type: PUSH_ROUTE, 23 | payload: { 24 | route, 25 | key 26 | } 27 | }; 28 | } 29 | 30 | export function popRoute(key) { 31 | if (!key) { 32 | throw new Error('popRoute requires key argument'); 33 | } 34 | 35 | return { 36 | type: POP_ROUTE, 37 | payload: { 38 | key 39 | } 40 | }; 41 | } 42 | 43 | export function jumpTo(keyOrIndex, key) { 44 | // XX: to make this backwards compatible, 45 | // jumpTo supports both key and index first arg 46 | // JUMP_TO action is used if the first arg is a string key 47 | // otherwise JUMP_TO_INDEX is used 48 | 49 | if (!key) { 50 | throw new Error('jumpTo requires key argument'); 51 | } 52 | 53 | if (typeof keyOrIndex === 'string') { 54 | return { 55 | type: JUMP_TO, 56 | payload: { 57 | routeKey: keyOrIndex, 58 | key 59 | } 60 | }; 61 | } 62 | 63 | return jumpToIndex(keyOrIndex, key); 64 | } 65 | 66 | export function reset(routes, key, index) { 67 | if (!key) { 68 | throw new Error('reset requires key argument'); 69 | } 70 | return { 71 | type: RESET_ROUTE, 72 | payload: { 73 | routes, 74 | index, 75 | key 76 | } 77 | } 78 | } 79 | 80 | export function replaceAt(routeKey, route, key) { 81 | if (!key) { 82 | throw new Error('Replace At requires key argument'); 83 | } 84 | 85 | return { 86 | type: REPLACE_AT, 87 | payload: { 88 | routeKey, 89 | route, 90 | key 91 | } 92 | } 93 | } 94 | 95 | export function replaceAtIndex(index, route, key) { 96 | if (!key) { 97 | throw new Error('Replace At Index requires key argument'); 98 | } 99 | 100 | return { 101 | type: REPLACE_AT_INDEX, 102 | payload: 103 | { 104 | index, 105 | route, 106 | key 107 | } 108 | } 109 | } 110 | 111 | 112 | export function jumpToIndex(routeIndex, key) { 113 | if (!key) { 114 | throw new Error('Jump to Index requires key argument'); 115 | } 116 | 117 | return { 118 | type: JUMP_TO_INDEX, 119 | payload: { 120 | routeIndex, 121 | key 122 | } 123 | } 124 | } 125 | 126 | export function back(key) { 127 | if (!key) { 128 | throw new Error('popRoute requires key argument'); 129 | } 130 | 131 | return { 132 | type: BACK, 133 | payload: { 134 | key 135 | } 136 | }; 137 | } 138 | 139 | export function forward(key) { 140 | if (!key) { 141 | throw new Error('popRoute requires key argument'); 142 | } 143 | 144 | return { 145 | type: FORWARD, 146 | payload: { 147 | key 148 | } 149 | }; 150 | } 151 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | const prefix = 'react-native-navigation-redux-helpers/'; 2 | 3 | export const JUMP_TO = `${prefix}JUMP_TO`; 4 | export const PUSH_ROUTE = `${prefix}PUSH_ROUTE`; 5 | export const POP_ROUTE = `${prefix}POP_ROUTE`; 6 | export const RESET_ROUTE = `${prefix}RESET_ROUTE`; 7 | export const REPLACE_AT = `${prefix}REPLACE_AT`; 8 | export const REPLACE_AT_INDEX = `${prefix}REPLACE_AT_INDEX`; 9 | export const JUMP_TO_INDEX = `${prefix}JUMP_TO_INDEX`; 10 | export const BACK = `${prefix}BACK`; 11 | export const FORWARD = `${prefix}FORWARD`; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | PUSH_ROUTE, 3 | POP_ROUTE, 4 | RESET_ROUTE, 5 | REPLACE_AT, 6 | REPLACE_AT_INDEX, 7 | JUMP_TO, 8 | JUMP_TO_INDEX, 9 | BACK, 10 | FORWARD, 11 | GET, 12 | HAS, 13 | INDEX_OF 14 | } from './constants'; 15 | 16 | import { 17 | pushRoute, 18 | popRoute, 19 | jumpTo, 20 | reset, 21 | replaceAt, 22 | replaceAtIndex, 23 | jumpToIndex, 24 | back, 25 | forward, 26 | get, 27 | has, 28 | indexOf 29 | } from './actions'; 30 | 31 | import { cardStackReducer as csr } from './reducers/card-stack'; 32 | import { tabReducer as tr } from './reducers/tab-reducer'; 33 | 34 | export const constants = { 35 | PUSH_ROUTE, 36 | POP_ROUTE, 37 | RESET_ROUTE, 38 | REPLACE_AT, 39 | REPLACE_AT_INDEX, 40 | JUMP_TO, 41 | JUMP_TO_INDEX, 42 | BACK, 43 | FORWARD, 44 | GET, 45 | HAS, 46 | INDEX_OF 47 | }; 48 | 49 | export const actions = { 50 | pushRoute, 51 | popRoute, 52 | jumpTo, 53 | reset, 54 | replaceAt, 55 | replaceAtIndex, 56 | jumpToIndex, 57 | back, 58 | forward, 59 | get, 60 | has, 61 | indexOf 62 | }; 63 | 64 | export const cardStackReducer = csr; 65 | export const tabReducer = tr; 66 | -------------------------------------------------------------------------------- /src/reducers/card-stack.js: -------------------------------------------------------------------------------- 1 | import { 2 | PUSH_ROUTE, 3 | POP_ROUTE, 4 | RESET_ROUTE, 5 | REPLACE_AT, 6 | REPLACE_AT_INDEX, 7 | JUMP_TO, 8 | JUMP_TO_INDEX, 9 | BACK, 10 | FORWARD 11 | } from '../constants'; 12 | 13 | import { 14 | checkInitialState, 15 | isActionPotentiallyApplicable, 16 | getStateUtils 17 | } from './helpers'; 18 | 19 | const StateUtils = getStateUtils(); 20 | 21 | export function cardStackReducer(initialState) { 22 | checkInitialState(initialState); 23 | 24 | // eslint-disable-next-line complexity 25 | return function cardStackReducerFn(state = initialState, action) { 26 | if (!isActionPotentiallyApplicable(action, state.key)) { 27 | return state; 28 | } 29 | 30 | switch (action.type) { 31 | case PUSH_ROUTE: 32 | if (state.routes[state.index].key === (action.payload && action.payload.route.key)) return state; 33 | return StateUtils.push(state, action.payload.route); 34 | case POP_ROUTE: 35 | return StateUtils.pop(state); 36 | case RESET_ROUTE: 37 | return StateUtils.reset(state, action.payload.routes, action.payload.index); 38 | case REPLACE_AT: 39 | return StateUtils.replaceAt(state, action.payload.routeKey, action.payload.route); 40 | case REPLACE_AT_INDEX: 41 | return StateUtils.replaceAtIndex(state, action.payload.index, action.payload.route); 42 | case JUMP_TO: 43 | return StateUtils.jumpTo(state, action.payload.routeKey); 44 | case JUMP_TO_INDEX: 45 | return StateUtils.jumpToIndex(state, action.payload.routeIndex); 46 | case BACK: 47 | return StateUtils.back(state); 48 | case FORWARD: 49 | return StateUtils.forward(state); 50 | default: 51 | return state; 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/reducers/helpers.js: -------------------------------------------------------------------------------- 1 | export function checkInitialState(initialState) { 2 | if (!initialState) { 3 | throw Error('initialState arg is required'); 4 | } 5 | 6 | if (typeof initialState.key !== 'string') { 7 | throw Error('initialState must have an attribute **key** which is a string'); 8 | } 9 | 10 | if (typeof initialState.index !== 'number') { 11 | throw Error('initialState must have an attribute **index** which is a number'); 12 | } 13 | 14 | if (!(initialState.routes instanceof Array)) { 15 | throw Error('initialState must have an attribute **route** which is an array'); 16 | } 17 | } 18 | 19 | export function isActionPotentiallyApplicable(action, navigationKey) { 20 | return action && action.payload && (action.payload.key === navigationKey); 21 | } 22 | 23 | export function getStateUtils() { 24 | try { 25 | const { NavigationExperimental } = require('react-native'); 26 | return NavigationExperimental.StateUtils; 27 | } catch(e) { 28 | // no-op 29 | } 30 | 31 | return {}; 32 | } 33 | -------------------------------------------------------------------------------- /src/reducers/tab-reducer.js: -------------------------------------------------------------------------------- 1 | import { checkInitialState, isActionPotentiallyApplicable, getStateUtils } from './helpers'; 2 | import { JUMP_TO, JUMP_TO_INDEX } from '../constants'; 3 | 4 | const StateUtils = getStateUtils(); 5 | 6 | export function tabReducer(initialState) { 7 | checkInitialState(initialState); 8 | 9 | return function tabReducerFn(state = initialState, action) { 10 | if (!isActionPotentiallyApplicable(action, state.key)) { 11 | return state; 12 | } 13 | 14 | switch(action.type) { 15 | case JUMP_TO: 16 | return StateUtils.jumpTo(state, action.payload.routeKey); 17 | case JUMP_TO_INDEX: 18 | return StateUtils.jumpToIndex(state, action.payload.routeIndex); 19 | default: 20 | return state; 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./setup/.globals.json", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module" 6 | }, 7 | "rules": { 8 | "strict": 0, 9 | "quotes": [2, "single"], 10 | "no-unused-expressions": 0 11 | }, 12 | "env": { 13 | "browser": true, 14 | "node": true, 15 | "mocha": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /test/setup/.globals.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "expect": true, 4 | "mock": true, 5 | "sandbox": true, 6 | "spy": true, 7 | "stub": true, 8 | "useFakeServer": true, 9 | "useFakeTimers": true, 10 | "useFakeXMLHttpRequest": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/setup/browser.js: -------------------------------------------------------------------------------- 1 | var mochaGlobals = require('./.globals.json').globals; 2 | 3 | window.mocha.setup('bdd'); 4 | window.onload = function() { 5 | window.mocha.checkLeaks(); 6 | window.mocha.globals(Object.keys(mochaGlobals)); 7 | window.mocha.run(); 8 | require('./setup')(window); 9 | }; 10 | -------------------------------------------------------------------------------- /test/setup/node.js: -------------------------------------------------------------------------------- 1 | global.chai = require('chai'); 2 | global.sinon = require('sinon'); 3 | global.chai.use(require('sinon-chai')); 4 | 5 | require('babel-core/register'); 6 | require('./setup')(); 7 | 8 | /* 9 | Uncomment the following if your library uses features of the DOM, 10 | for example if writing a jQuery extension, and 11 | add 'simple-jsdom' to the `devDependencies` of your package.json 12 | 13 | Note that JSDom doesn't implement the entire DOM API. If you're using 14 | more advanced or experimental features, you may need to switch to 15 | PhantomJS. Setting that up is currently outside of the scope of this 16 | boilerplate. 17 | */ 18 | // import simpleJSDom from 'simple-jsdom'; 19 | // simpleJSDom.install(); 20 | -------------------------------------------------------------------------------- /test/setup/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = function(root) { 2 | root = root ? root : global; 3 | root.expect = root.chai.expect; 4 | 5 | beforeEach(function() { 6 | // Using these globally-available Sinon features is preferrable, as they're 7 | // automatically restored for you in the subsequent `afterEach` 8 | root.sandbox = root.sinon.sandbox.create(); 9 | root.stub = root.sandbox.stub.bind(root.sandbox); 10 | root.spy = root.sandbox.spy.bind(root.sandbox); 11 | root.mock = root.sandbox.mock.bind(root.sandbox); 12 | root.useFakeTimers = root.sandbox.useFakeTimers.bind(root.sandbox); 13 | root.useFakeXMLHttpRequest = root.sandbox.useFakeXMLHttpRequest.bind(root.sandbox); 14 | root.useFakeServer = root.sandbox.useFakeServer.bind(root.sandbox); 15 | }); 16 | 17 | afterEach(function() { 18 | delete root.stub; 19 | delete root.spy; 20 | root.sandbox.restore(); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /test/unit/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | pushRoute, 3 | popRoute, 4 | jumpTo, 5 | reset, 6 | replaceAt, 7 | replaceAtIndex, 8 | jumpToIndex, 9 | back, 10 | forward 11 | } from '../../src/actions'; 12 | import { 13 | JUMP_TO, 14 | PUSH_ROUTE, 15 | POP_ROUTE, 16 | RESET_ROUTE, 17 | REPLACE_AT, 18 | REPLACE_AT_INDEX, 19 | JUMP_TO_INDEX, 20 | BACK, 21 | FORWARD 22 | } from '../../src/constants'; 23 | 24 | const navigationKey = 'nav-key'; 25 | 26 | describe('actions', () => { 27 | describe('definitions', () => { 28 | it('pushRoute action is defined', () => { 29 | expect(pushRoute).to.be.ok; 30 | expect(typeof pushRoute === 'function').to.be.true; 31 | }); 32 | 33 | it('popRoute action is defined', () => { 34 | expect(popRoute).to.be.ok; 35 | expect(typeof popRoute === 'function').to.be.true; 36 | }); 37 | 38 | it('jumpTo action is defined', () => { 39 | expect(jumpTo).to.be.ok; 40 | expect(typeof jumpTo === 'function').to.be.true; 41 | }); 42 | 43 | it('reset action is defined', () => { 44 | expect(reset).to.be.ok; 45 | expect(typeof reset === 'function').to.be.true; 46 | }); 47 | }); 48 | 49 | describe('all actions', () => { 50 | it('require key attribute', () => { 51 | const pushRouteFn = () => pushRoute({}); 52 | const popRouteFn = () => popRoute(); 53 | const jumpToFn = () => jumpTo(); 54 | 55 | expect(pushRouteFn).to.throw(Error); 56 | expect(popRouteFn).to.throw(Error); 57 | expect(jumpToFn).to.throw(Error); 58 | }); 59 | }); 60 | 61 | describe('pushRoute', () => { 62 | it('returns a message with type set to PUSH_ROUTE and appropriate payload', () => { 63 | const route = { key: 'route', data : {} }; 64 | const actionData = pushRoute(route, navigationKey); 65 | 66 | expect(actionData.type).to.equal(PUSH_ROUTE); 67 | expect(actionData.payload).to.be.ok; 68 | expect(actionData.payload.route).to.equal(route); 69 | expect(actionData.payload.key).to.equal(navigationKey); 70 | }); 71 | }); 72 | 73 | describe('popRoute', () => { 74 | it('returns a message with type set to POP_ROUTE and appropriate payload', () => { 75 | const actionData = popRoute(navigationKey); 76 | 77 | expect(actionData.type).to.equal(POP_ROUTE); 78 | expect(actionData.payload.key).to.equal(navigationKey); 79 | }); 80 | }); 81 | 82 | describe('jumpTo', () => { 83 | it('returns a message with type set to JUMP_TO_INDEX and appropriate payload with index first arg', () => { 84 | const tabIndex = 3; 85 | const actionData = jumpTo(tabIndex, navigationKey); 86 | 87 | expect(actionData.type).to.equal(JUMP_TO_INDEX); 88 | expect(actionData.payload).to.be.ok; 89 | expect(actionData.payload.routeIndex).to.equal(tabIndex); 90 | expect(actionData.payload.key).to.equal(navigationKey); 91 | }); 92 | 93 | it('supports string key first argument and returns message with type JUMP_TO and proper payload', () => { 94 | const routeKey = 'key'; 95 | const actionData = jumpTo(routeKey, navigationKey); 96 | 97 | expect(actionData.type).to.equal(JUMP_TO); 98 | expect(actionData.payload).to.be.ok; 99 | expect(actionData.payload.routeKey).to.equal(routeKey); 100 | expect(actionData.payload.key).to.equal(navigationKey); 101 | }); 102 | }); 103 | 104 | describe('reset', () => { 105 | it('returns a message with type set to RESET_ROUTE', () => { 106 | const routes = [{ key: 'route1' }]; 107 | const actionData = reset(routes, navigationKey); 108 | expect(actionData.type).to.equal(RESET_ROUTE); 109 | expect(actionData.payload).to.be.ok; 110 | expect(actionData.payload.key).to.equal(navigationKey); 111 | expect(actionData.payload.routes).to.equal(routes); 112 | }); 113 | 114 | it('returns a message with payload.index set to index passed as second arg', () => { 115 | const routes = [{ key: 'route1' }]; 116 | const actionData = reset(routes, navigationKey, 1); 117 | expect(actionData.payload).to.be.ok; 118 | expect(actionData.payload.routes).to.equal(routes); 119 | expect(actionData.payload.index).to.equal(1); 120 | }); 121 | }); 122 | 123 | describe('replaceAt', () => { 124 | it('returns a message with type set to REPLACE_AT + proper payload', () => { 125 | const route = { key: 'new route' }; 126 | const routeKey = 'old route'; 127 | const actionData = replaceAt(routeKey, route, navigationKey); 128 | expect(actionData.type).to.equal(REPLACE_AT); 129 | expect(actionData.payload).to.be.ok; 130 | expect(actionData.payload.routeKey).to.equal(routeKey); 131 | expect(actionData.payload.route).to.equal(route); 132 | }); 133 | }); 134 | 135 | describe('replaceAtIndex', () => { 136 | it('returns a message with type set to REPLACE_AT_INDEX + proper payload', () => { 137 | const route = { key: 'new route' }; 138 | const index = 1; 139 | const actionData = replaceAtIndex(index, route, navigationKey); 140 | expect(actionData.type).to.equal(REPLACE_AT_INDEX); 141 | expect(actionData.payload).to.be.ok; 142 | expect(actionData.payload.index).to.equal(index); 143 | expect(actionData.payload.route).to.equal(route); 144 | }); 145 | }); 146 | 147 | describe('jumpToIndex', () => { 148 | it('returns a message with type set to JUMP_TO_INDEX + proper payload', () => { 149 | const index = 1; 150 | const actionData = jumpToIndex(index, navigationKey); 151 | expect(actionData.type).to.equal(JUMP_TO_INDEX); 152 | expect(actionData.payload).to.be.ok; 153 | expect(actionData.payload.routeIndex).to.equal(index); 154 | }); 155 | }); 156 | 157 | describe('back', () => { 158 | it('returns a message with type set to BACK + proper payload', () => { 159 | const actionData = back(navigationKey); 160 | expect(actionData.type).to.equal(BACK); 161 | expect(actionData.payload).to.be.ok; 162 | expect(actionData.payload.key).to.equal(navigationKey); 163 | }); 164 | }); 165 | 166 | describe('forward', () => { 167 | it('returns a message with type set to FORWARD + proper payload', () => { 168 | const actionData = forward(navigationKey); 169 | expect(actionData.type).to.equal(FORWARD); 170 | expect(actionData.payload).to.be.ok; 171 | expect(actionData.payload.key).to.equal(navigationKey); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/unit/constants.js: -------------------------------------------------------------------------------- 1 | import { 2 | JUMP_TO, 3 | PUSH_ROUTE, 4 | POP_ROUTE, 5 | RESET_ROUTE, 6 | REPLACE_AT, 7 | REPLACE_AT_INDEX, 8 | JUMP_TO_INDEX, 9 | BACK, 10 | FORWARD 11 | } from '../../src/constants'; 12 | 13 | describe('constants', () => { 14 | describe('definitions', () => { 15 | it('JUMP_TO is defined', () => { 16 | expect(JUMP_TO).to.be.ok; 17 | }); 18 | 19 | it('PUSH_ROUTE is defined', () => { 20 | expect(PUSH_ROUTE).to.be.ok; 21 | }); 22 | 23 | it('POP_ROUTE is defined', () => { 24 | expect(POP_ROUTE).to.be.ok; 25 | }); 26 | 27 | it('RESET_ROUTE is defined', () => { 28 | expect(RESET_ROUTE).to.be.ok; 29 | }); 30 | 31 | it('REPLACE_AT is defined', () => { 32 | expect(REPLACE_AT).to.be.ok; 33 | }); 34 | 35 | it('REPLACE_AT_INDEX is defined', () => { 36 | expect(REPLACE_AT_INDEX).to.be.ok; 37 | }); 38 | 39 | it('JUMP_TO_INDEX is defined', () => { 40 | expect(JUMP_TO_INDEX).to.be.ok; 41 | }); 42 | 43 | it('BACK is defined', () => { 44 | expect(BACK).to.be.ok; 45 | }); 46 | 47 | it('FORWARD is defined', () => { 48 | expect(FORWARD).to.be.ok; 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | constants, actions, cardStackReducer, tabReducer 3 | } from '../../src/index'; 4 | 5 | const { 6 | JUMP_TO, 7 | PUSH_ROUTE, 8 | POP_ROUTE, 9 | RESET_ROUTE, 10 | REPLACE_AT, 11 | REPLACE_AT_INDEX, 12 | JUMP_TO_INDEX, 13 | BACK, 14 | FORWARD 15 | } = constants; 16 | 17 | const { 18 | pushRoute, 19 | popRoute, 20 | jumpTo, 21 | reset, 22 | replaceAt, 23 | replaceAtIndex, 24 | jumpToIndex, 25 | back, 26 | forward 27 | } = actions; 28 | 29 | describe('react-native-navigation-redux-helpers', () => { 30 | describe('definitions', () => { 31 | it('exports constants', () => { 32 | expect(constants).to.be.ok; 33 | 34 | [ 35 | JUMP_TO, 36 | PUSH_ROUTE, 37 | POP_ROUTE, 38 | RESET_ROUTE, 39 | REPLACE_AT, 40 | REPLACE_AT_INDEX, 41 | JUMP_TO_INDEX, 42 | BACK, 43 | FORWARD 44 | ].forEach(c => expect(c).to.be.ok); 45 | }); 46 | 47 | it('exports actions', () => { 48 | expect(actions).to.be.ok; 49 | 50 | [ 51 | pushRoute, 52 | popRoute, 53 | jumpTo, 54 | reset, 55 | replaceAt, 56 | replaceAtIndex, 57 | jumpToIndex, 58 | back, 59 | forward 60 | ].forEach(a => expect(a).to.be.ok); 61 | }); 62 | 63 | it('exports cardStackReducer', () => { 64 | expect(cardStackReducer).to.be.ok; 65 | }); 66 | 67 | it('exports tabReducer', () => { 68 | expect(tabReducer).to.be.ok; 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/unit/reducers.js: -------------------------------------------------------------------------------- 1 | import { __RewireAPI__ as cardStackReducerAPI, cardStackReducer } from '../../src/reducers/card-stack'; 2 | import { __RewireAPI__ as tabReducerAPI, tabReducer } from '../../src/reducers/tab-reducer'; 3 | import { 4 | pushRoute, 5 | popRoute, 6 | jumpTo, 7 | reset, 8 | replaceAt, 9 | replaceAtIndex, 10 | jumpToIndex, 11 | back, 12 | forward 13 | } from '../../src/actions'; 14 | 15 | const cardStackInitialState = { 16 | key: 'key', 17 | index: 0, 18 | routes: [{ 19 | key: 'route-1', 20 | title: 'Route 1' 21 | }] 22 | }; 23 | 24 | const repeatedRoute = { 25 | key: 'route-1', 26 | title: 'Route 1' 27 | }; 28 | 29 | const tabInitialState = { 30 | key: 'ta-key', 31 | index: 0, 32 | routes: [ 33 | { 34 | key: 'route-1', 35 | title: 'Route 1' 36 | }, 37 | { 38 | key: 'route-2', 39 | title: 'Route 2' 40 | } 41 | ] 42 | } 43 | 44 | describe('reducers', () => { 45 | let pushSpy, popSpy, jumpToIndexSpy, jumpToSpy, 46 | resetSpy, replaceAtSpy, replaceAtIndexSpy, 47 | backSpy, forwardSpy; 48 | 49 | const StateUtils = { 50 | push(state, action) { 51 | return 'StateUtils.push'; 52 | }, 53 | 54 | pop(state) { 55 | return 'StateUtils.pop'; 56 | }, 57 | 58 | jumpTo(state, key) { 59 | return 'StateUtils.jumpTo'; 60 | }, 61 | 62 | jumpToIndex(state, index) { 63 | return 'StateUtils.jumpToIndex'; 64 | }, 65 | 66 | reset(state, routes, index) { 67 | return 'StateUtils.reset'; 68 | }, 69 | 70 | replaceAt(state, key, route) { 71 | return 'StateUtils.replaceAt'; 72 | }, 73 | 74 | replaceAtIndex(state, index, route) { 75 | return 'StateUtils.replaceAtIndex'; 76 | }, 77 | 78 | back(state) { 79 | return 'StateUtils.back'; 80 | }, 81 | 82 | forward(state) { 83 | return 'StateUtils.forward'; 84 | } 85 | }; 86 | 87 | describe('definitions', () => { 88 | it('cardStackReducer is defined and it is a function', () => { 89 | expect(cardStackReducer).to.be.ok; 90 | expect(typeof cardStackReducer === 'function').to.be.true; 91 | }); 92 | 93 | it('tabReducer is defined and it is a function', () => { 94 | expect(tabReducer).to.be.ok; 95 | expect(typeof tabReducer === 'function').to.be.true; 96 | }); 97 | }); 98 | 99 | describe('cardStackReducer', () => { 100 | let reducer; 101 | 102 | beforeEach(() => { 103 | reducer = cardStackReducer(cardStackInitialState); 104 | 105 | cardStackReducerAPI.__Rewire__('StateUtils', StateUtils); 106 | pushSpy = spy(StateUtils, 'push'); 107 | popSpy = spy(StateUtils, 'pop'); 108 | resetSpy = spy(StateUtils, 'reset'); 109 | replaceAtSpy = spy(StateUtils, 'replaceAt'); 110 | replaceAtIndexSpy = spy(StateUtils, 'replaceAtIndex'); 111 | jumpToSpy = spy(StateUtils, 'jumpTo'); 112 | jumpToIndexSpy = spy(StateUtils, 'jumpToIndex'); 113 | backSpy = spy(StateUtils, 'back'); 114 | forwardSpy = spy(StateUtils, 'forward'); 115 | }); 116 | 117 | afterEach(() => { 118 | StateUtils.push.restore(); 119 | StateUtils.pop.restore(); 120 | StateUtils.reset.restore(); 121 | StateUtils.replaceAt.restore(); 122 | StateUtils.replaceAtIndex.restore(); 123 | StateUtils.jumpTo.restore(); 124 | StateUtils.jumpToIndex.restore(); 125 | StateUtils.back.restore(); 126 | StateUtils.forward.restore(); 127 | cardStackReducerAPI.__ResetDependency__('StateUtils'); 128 | }); 129 | 130 | it('throws if initial state does not look good', () => { 131 | const cardStackReducerFnNoInitialState = () => cardStackReducer(); 132 | const cardStackReducerFnJustKey = () => cardStackReducer({ key: 'key' }); 133 | const cardStackReducerFnNoRoutes = () => cardStackReducer({ key: 'key', index: 0 }); 134 | 135 | expect(cardStackReducerFnNoInitialState).to.throw(Error); 136 | expect(cardStackReducerFnJustKey).to.throw(Error); 137 | expect(cardStackReducerFnNoRoutes).to.throw(Error); 138 | }); 139 | 140 | it('returns a function', () => { 141 | expect(typeof reducer === 'function').to.be.true; 142 | }); 143 | 144 | it('returns navigation state for random events', () => { 145 | expect(reducer(cardStackInitialState, null)).to.equal(cardStackInitialState); 146 | expect(reducer(cardStackInitialState, { type: 'RANDOM_EVENT' })).to.equal(cardStackInitialState); 147 | }); 148 | 149 | it('calls RN\'s StateUtils.push when pushRoute action arrives and returns whatever StateUtils.push returns', () => { 150 | const action = pushRoute({ key: 'route' }, cardStackInitialState.key); 151 | 152 | const returnValue = reducer(cardStackInitialState, action); 153 | 154 | expect(pushSpy).to.have.been.calledOnce; 155 | expect(pushSpy).to.have.been.calledWith(cardStackInitialState, action.payload.route); 156 | expect(returnValue).to.equal('StateUtils.push'); 157 | }); 158 | 159 | it('does not call RN\'s StateUtils.push when pushRoute action has payload.key same with current route state.key and returns current nav state', () => { 160 | const action = pushRoute({ key: 'route' }, repeatedRoute.key); 161 | 162 | const returnValue = reducer(cardStackInitialState, action); 163 | expect(pushSpy.callCount).to.equal(0); 164 | expect(returnValue).to.equal(cardStackInitialState); 165 | }); 166 | 167 | it('does not call RN\'s StateUtils.push when pushRoute action has payload.key different from state.key and returns current nav state', () => { 168 | const action = pushRoute({ key: 'route' }, 'nav'); 169 | 170 | const returnValue = reducer(cardStackInitialState, action); 171 | expect(pushSpy.callCount).to.equal(0); 172 | expect(returnValue).to.equal(cardStackInitialState); 173 | }); 174 | 175 | it('calls RN\'s StateUtils.pop when popRoute action arrives and returns whatever StateUtils.pop returns', () => { 176 | const action = popRoute(cardStackInitialState.key); 177 | const returnValue = reducer(cardStackInitialState, action); 178 | 179 | expect(popSpy).to.have.been.calledOnce; 180 | expect(popSpy).to.have.been.calledWith(cardStackInitialState); 181 | expect(returnValue).to.equal('StateUtils.pop'); 182 | }); 183 | 184 | it('does not call RN\'s StateUtils.pop when popRoute action has payload.key different from state.key and returns current nav state', () => { 185 | const action = popRoute('random-nav-key'); 186 | const returnValue = reducer(cardStackInitialState, action); 187 | 188 | expect(popSpy.callCount).to.equal(0); 189 | expect(returnValue).to.equal(cardStackInitialState); 190 | }); 191 | 192 | it('calls RN\'s StateUtils.reset when reset action arrives', () => { 193 | const routes = [{ key: 'new route'}]; 194 | const action = reset(routes, cardStackInitialState.key); 195 | const returnValue = reducer(cardStackInitialState, action); 196 | 197 | expect(resetSpy).to.have.been.calledOnce; 198 | expect(resetSpy).to.have.been.calledWith( 199 | cardStackInitialState, routes); 200 | }); 201 | 202 | it('calls RN\'s StateUtils.reset with index when reset action has index data', () => { 203 | const routes = [{ key: 'new route'}]; 204 | const action = reset(routes, cardStackInitialState.key, 0); 205 | reducer(cardStackInitialState, action); 206 | 207 | expect(resetSpy).to.have.been.calledOnce; 208 | expect(resetSpy).to.have.been.calledWith( 209 | cardStackInitialState, routes, 0); 210 | }); 211 | 212 | it('calls RN\'s StateUtils.replaceAt when replaceAt action arrives', () => { 213 | const routeKey = 'old-key'; 214 | const route = { key: 'new-route' }; 215 | const action = replaceAt(routeKey, route, cardStackInitialState.key); 216 | reducer(cardStackInitialState, action); 217 | 218 | expect(replaceAtSpy).to.have.been.calledOnce; 219 | expect(replaceAtSpy).to.have.been.calledWith( 220 | cardStackInitialState, routeKey, route 221 | ); 222 | }); 223 | 224 | it('calls RN\'s StateUtils.replaceAtIndex when replaceAtIndex action arrives', () => { 225 | const index = 1; 226 | const route = { key: 'new-route' }; 227 | const action = replaceAtIndex(index, route, cardStackInitialState.key); 228 | reducer(cardStackInitialState, action); 229 | 230 | expect(replaceAtIndexSpy).to.have.been.calledOnce; 231 | expect(replaceAtIndexSpy).to.have.been.calledWith( 232 | cardStackInitialState, index, route 233 | ); 234 | }); 235 | 236 | it('calls RN\'s StateUtils.jumpToIndex when jumpToIndex action arrives', () => { 237 | const action = jumpToIndex(0, cardStackInitialState.key); 238 | reducer(cardStackInitialState, action); 239 | 240 | expect(jumpToIndexSpy).to.have.been.calledOnce; 241 | expect(jumpToIndexSpy).to.have.been.calledWith(cardStackInitialState, action.payload.routeIndex); 242 | }); 243 | 244 | it('calls RN\'s StateUtils.jumpTo when jumpTo action arrives', () => { 245 | const routeKey = 'key'; 246 | const action = jumpTo(routeKey, cardStackInitialState.key); 247 | reducer(cardStackInitialState, action); 248 | 249 | expect(jumpToSpy).to.have.been.calledOnce; 250 | expect(jumpToSpy).to.have.been.calledWith(cardStackInitialState, action.payload.routeKey); 251 | }); 252 | 253 | it('calls RN\'s StateUtils.back when back action arrives', () => { 254 | const action = back(cardStackInitialState.key); 255 | reducer(cardStackInitialState, action); 256 | 257 | expect(backSpy).to.have.been.calledOnce; 258 | expect(backSpy).to.have.been.calledWith(cardStackInitialState); 259 | }); 260 | 261 | it('calls RN\'s StateUtils.forward when forward action arrives', () => { 262 | const action = forward(cardStackInitialState.key); 263 | reducer(cardStackInitialState, action); 264 | 265 | expect(forwardSpy).to.have.been.calledOnce; 266 | expect(forwardSpy).to.have.been.calledWith(cardStackInitialState); 267 | }); 268 | }); 269 | 270 | describe('tabReducer', () => { 271 | let reducer; 272 | 273 | beforeEach(() => { 274 | reducer = tabReducer(tabInitialState); 275 | tabReducerAPI.__Rewire__('StateUtils', StateUtils); 276 | jumpToIndexSpy = spy(StateUtils, 'jumpToIndex'); 277 | jumpToSpy = spy(StateUtils, 'jumpTo'); 278 | }); 279 | 280 | afterEach(() => { 281 | StateUtils.jumpToIndex.restore(); 282 | StateUtils.jumpTo.restore(); 283 | tabReducerAPI.__ResetDependency__('StateUtils'); 284 | }); 285 | 286 | it('throws if initial state does not look good', () => { 287 | const tabReducerFnNoInitialState = () => tabReducer(); 288 | const tabReducerFnJustKey = () => tabReducer({ key: 'key' }); 289 | const tabReducerFnNoRoutes = () => tabReducer({ key: 'key', index: 0 }); 290 | 291 | expect(tabReducerFnNoInitialState).to.throw(Error); 292 | expect(tabReducerFnJustKey).to.throw(Error); 293 | expect(tabReducerFnNoRoutes).to.throw(Error); 294 | }); 295 | 296 | it('returns a function', () => { 297 | expect(typeof reducer === 'function').to.be.true; 298 | }); 299 | 300 | it('returns navigation state for random events', () => { 301 | expect(reducer(tabInitialState, null)).to.equal(tabInitialState); 302 | expect(reducer(tabInitialState, { type: 'RANDOM_EVENT' })).to.equal(tabInitialState); 303 | }); 304 | 305 | it('calls RN\'s StateUtils.jumpToIndex when jumpToIndex action arrives and returns whatever StateUtils.jumpToIndex returns', () => { 306 | const action = jumpToIndex(0, tabInitialState.key); 307 | const returnValue = reducer(tabInitialState, action); 308 | 309 | expect(jumpToIndexSpy).to.have.been.calledOnce; 310 | expect(jumpToIndexSpy).to.have.been.calledWith(tabInitialState, action.payload.routeIndex); 311 | expect(returnValue).to.equal('StateUtils.jumpToIndex'); 312 | }); 313 | 314 | it('does not call RN\'s StateUtils.jumpToIndex when jumpToIndex action has key different from state.key and returns current nav state', () => { 315 | const action = jumpToIndex(0, 'a different key'); 316 | const returnValue = reducer(tabInitialState, action); 317 | 318 | expect(jumpToIndexSpy.callCount).to.equal(0); 319 | expect(returnValue).to.equal(tabInitialState); 320 | }); 321 | 322 | it('calls RN\'s StateUtils.jumpTo when jumpTo action arrives', () => { 323 | const routeKey = 'key'; 324 | const action = jumpTo(routeKey, tabInitialState.key); 325 | reducer(tabInitialState, action); 326 | 327 | expect(jumpToSpy).to.have.been.calledOnce; 328 | expect(jumpToSpy).to.have.been.calledWith(tabInitialState, action.payload.routeKey); 329 | }); 330 | }); 331 | }); 332 | --------------------------------------------------------------------------------