From e0504577dbb0e5c0844466a346fbc1de2f2f4c22 Mon Sep 17 00:00:00 2001 From: prescottprue Date: Mon, 11 Dec 2017 01:31:56 -0800 Subject: [PATCH 1/8] Removed recompose's withContext from withFirebase and withFirestore - #337 * exposed reducer (matches common redux panther also seen with other libraries such as redux-form) --- package.json | 2 +- src/index.js | 1 + src/withFirebase.js | 39 ++++++++++++++++++++--------- src/withFirestore.js | 59 ++++++++++++++++++++++++++++---------------- src/withStore.js | 9 ------- 5 files changed, 68 insertions(+), 42 deletions(-) delete mode 100644 src/withStore.js diff --git a/package.json b/package.json index 41c0a708a..a8dd0c5a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-redux-firebase", - "version": "2.0.0-beta.19", + "version": "2.0.0-rc.1", "description": "Redux integration for Firebase. Comes with a Higher Order Components for use with React.", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/index.js b/src/index.js index f5e339c43..3406639a6 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ export default { createWithFirebase, withFirestore, createWithFirestore, + reducer, firebaseReducer: reducer, firebaseStateReducer: reducer, reduxReactFirebase: enhancer, diff --git a/src/withFirebase.js b/src/withFirebase.js index f3c41fb53..4667c1444 100644 --- a/src/withFirebase.js +++ b/src/withFirebase.js @@ -1,11 +1,12 @@ -import { compose, withProps } from 'recompose' -import { createWithStore } from './withStore' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import hoistStatics from 'hoist-non-react-statics' +import { wrapDisplayName } from 'recompose' /** * @name createWithFirebase * @description Function that creates a Higher Order Component that - * automatically listens/unListens to provided firebase paths using - * React's Lifecycle hooks. + * which providers props.firebase to React Components. * **WARNING!!** This is an advanced feature, and should only be used when * needing to access a firebase instance created under a different store key. * @param {String} [storeKey='store'] - Name of redux store which contains @@ -21,13 +22,29 @@ import { createWithStore } from './withStore' * // use the withFirebase to wrap a component * export default withFirebase(SomeComponent) */ -export const createWithFirebase = (storeKey) => compose( - createWithStore(storeKey), - withProps(({ store: { firebase, dispatch } }) => ({ - firebase, - dispatch - })) -) +export const createWithFirebase = (storeKey = 'store') => WrappedComponent => { + class withFirebase extends Component { + static wrappedComponent = WrappedComponent + static displayName = wrapDisplayName(WrappedComponent, 'withFirebase') + static contextTypes = { + [storeKey]: PropTypes.object.isRequired + } + + store = this.context[storeKey] + + render () { + return ( + + ) + } + } + + return hoistStatics(withFirebase, WrappedComponent) +} /** * @name withFirebase diff --git a/src/withFirestore.js b/src/withFirestore.js index 74479b7d5..a6a185968 100644 --- a/src/withFirestore.js +++ b/src/withFirestore.js @@ -1,11 +1,11 @@ -import { compose, withProps } from 'recompose' -import { createWithStore } from './withStore' - +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import hoistStatics from 'hoist-non-react-statics' +import { wrapDisplayName } from 'recompose' /** * @name createWithFirestore * @description Function that creates a Higher Order Component that - * automatically listens/unListens to provided firebase paths using - * React's Lifecycle hooks. + * which providers props.firestore and props.firebase to React Components. * **WARNING!!** This is an advanced feature, and should only be used when * needing to access a firebase instance created under a different store key. * @param {String} [storeKey='store'] - Name of redux store which contains @@ -15,30 +15,47 @@ import { createWithStore } from './withStore' * // this.props.firebase set on App component as firebase object with helpers * import { createWithFirebase } from 'react-redux-firebase' * - * // create withFirebase that uses another redux store - * const withFirebase = createWithFirebase('anotherStore') + * // create withFirestore that uses another redux store + * const withFirestore = createWithFirebase('anotherStore') * - * // use the withFirebase to wrap a component - * export default withFirebase(SomeComponent) + * // use the withFirestore to wrap a component + * export default withFirestore(SomeComponent) */ -export const createWithFirestore = (storeKey) => compose( - createWithStore(storeKey), - withProps(({ store: { firebase, firestore, dispatch } }) => ({ - firebase: { ...firebase, ...firebase.helpers }, - dispatch, - firestore - })) -) +export const createWithFirestore = (storeKey = 'store') => WrappedComponent => { + class withFirestore extends Component { + static wrappedComponent = WrappedComponent + static displayName = wrapDisplayName(WrappedComponent, 'withFirestore') + static contextTypes = { + [storeKey]: PropTypes.object.isRequired + } + + store = this.context[storeKey] + + render () { + return ( + + ) + } + } + + return hoistStatics(withFirestore, WrappedComponent) +} /** - * @name withFirebase + * @name withFirestore * @extends React.Component - * @description Higher Order Component that attaches firebase to props. + * @description Higher Order Component that attaches props.firestore and + * props.firebase to React Components. * Firebase is gathered from store.firebase, which is attached to store by * the store enhancer (reactReduxFirebase) in ./enhancer. * @return {Function} - That accepts a component to wrap and returns the wrapped component * @example Basic - * import { withFirebase } from 'react-redux-firebase' + * import { withFirestore } from 'react-redux-firebase' * * const AddData = ({ firebase: { push } }) => *
@@ -47,6 +64,6 @@ export const createWithFirestore = (storeKey) => compose( * *
* - * export default withFirebase(AddData) + * export default withFirestore(AddData) */ export default createWithFirestore() diff --git a/src/withStore.js b/src/withStore.js deleted file mode 100644 index b87bba0c9..000000000 --- a/src/withStore.js +++ /dev/null @@ -1,9 +0,0 @@ -import PropTypes from 'prop-types' -import { compose, withContext, getContext } from 'recompose' - -export const createWithStore = (storeKey = 'store') => compose( - withContext({ [storeKey]: PropTypes.object }, () => {}), - getContext({ [storeKey]: PropTypes.object }) -) - -export default createWithStore() From 759a1b10ea3ec981621041c6cf4294cedde5708f Mon Sep 17 00:00:00 2001 From: Scott Prue Date: Mon, 11 Dec 2017 23:48:17 -0800 Subject: [PATCH 2/8] Tests added for withFirestore. --- package.json | 3 ++ src/withFirestore.js | 1 + tests/setup.js | 16 ++++---- tests/unit/withFirestore.spec.js | 64 ++++++++++++++++++++++++++++++++ tests/utils/components.js | 60 ++++++++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 tests/unit/withFirestore.spec.js create mode 100644 tests/utils/components.js diff --git a/package.json b/package.json index a8dd0c5a5..043ff3afe 100644 --- a/package.json +++ b/package.json @@ -82,12 +82,14 @@ "babel-register": "^6.24.0", "chai": "^3.5.0", "chai-as-promised": "^6.0.0", + "chai-enzyme": "^0.8.0", "child-process-promise": "^2.2.1", "codecov": "^2.1.0", "cross-env": "^5.1.0", "docdown": "^0.7.2", "documentation": "^4.0.0-beta15", "documentation-markdown-api-theme": "^1.0.2", + "enzyme": "^2.3.0", "eslint": "^3.19.0", "eslint-config-standard": "^10.0.0", "eslint-config-standard-react": "^4.3.0", @@ -107,6 +109,7 @@ "react-addons-test-utils": "^15.4.2", "react-dom": "^15.4.2", "redux": "3.6.0", + "redux-firestore": "^0.2.0", "rimraf": "^2.6.2", "sinon": "^2.1.0", "sinon-chai": "^2.9.0", diff --git a/src/withFirestore.js b/src/withFirestore.js index a6a185968..20b94c6cc 100644 --- a/src/withFirestore.js +++ b/src/withFirestore.js @@ -38,6 +38,7 @@ export const createWithFirestore = (storeKey = 'store') => WrappedComponent => { {...this.state} firestore={this.store.firestore} firebase={this.store.firebase} + dispatch={this.store.dispatch} /> ) } diff --git a/tests/setup.js b/tests/setup.js index abac34e1e..5652d7123 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,13 +1,14 @@ /* eslint-disable no-unused-vars */ process.env.NODE_ENV = 'test' -var chai = require('chai') -var sinon = require('sinon') -var chaiAsPromised = require('chai-as-promised') -var sinonChai = require('sinon-chai') -var jsdom = require('jsdom').jsdom -var FirebaseServer = require('firebase-server') -var uid = global.uid = 'Iq5b0qK2NtgggT6U3bU6iZRGyma2' +const chai = require('chai') +const sinon = require('sinon') +const chaiAsPromised = require('chai-as-promised') +const sinonChai = require('sinon-chai') +const jsdom = require('jsdom').jsdom +const chaiEnzyme = require('chai-enzyme') +const FirebaseServer = require('firebase-server') +const uid = global.uid = 'Iq5b0qK2NtgggT6U3bU6iZRGyma2' new FirebaseServer(5000, 'localhost.firebaseio.test', { // eslint-disable-line no-new users: { @@ -20,6 +21,7 @@ new FirebaseServer(5000, 'localhost.firebaseio.test', { // eslint-disable-line n // Chai Plugins chai.use(chaiAsPromised) chai.use(sinonChai) +chai.use(chaiEnzyme()) // globals global.expect = chai.expect diff --git a/tests/unit/withFirestore.spec.js b/tests/unit/withFirestore.spec.js new file mode 100644 index 000000000..fd0fef676 --- /dev/null +++ b/tests/unit/withFirestore.spec.js @@ -0,0 +1,64 @@ +import React, { createElement } from 'react' +import TestUtils from 'react-addons-test-utils' +import { createSink, nest, renderComponent } from 'recompose' +import {mount, render, shallow} from 'enzyme' +import { TestContainer, ProviderMock, storeWithFirestore } from '../utils/components' +import withFirestore, { createWithFirestore } from '../../src/withFirestore' + +let store +let TestComponent +let wrapper + +describe('withFirestore', () => { + beforeEach(() => { + store = storeWithFirestore() + TestComponent = withFirestore(createSink) + wrapper = shallow(, { context: { store } }) + }) + + it('adds firestore as prop', () => { + expect(wrapper.prop('firestore')).to.exist + expect(wrapper.prop('firestore')).to.respondTo('add') + }) + + it('adds firebase as prop', () => { + expect(wrapper.prop('firebase')).to.exist + expect(wrapper.prop('firebase')).to.respondTo('push') + }) + + it('adds dispatch as prop', () => { + expect(wrapper.prop('dispatch')).to.exist + expect(wrapper.prop('dispatch')).to.be.a.function + }) + + describe('sets displayName static as', () => { + describe('withFirestore(${WrappedComponentName}) for', () => { // eslint-disable-line no-template-curly-in-string + it.skip('standard components', () => { + wrapper = shallow(, { context: { store } }) + expect(wrapper.instance.displayName).to.equal(`withFirestore(TestContainer)`) + }) + + it('string components', () => { + const str = 'Test' + const stringComp = withFirestore(str) + expect(stringComp.displayName).to.equal(`withFirestore(${str})`) + }) + }) + + it.skip('"Component" for all other types', () => { + wrapper = shallow(withFirestore()(
)) + expect(wrapper.displayName).to.equal('withFirestore(Component)') + }) + }) + + it.skip('sets WrappedComponent static as component which was wrapped', () => { + wrapper = shallow(, { context: { store } }) + expect(wrapper.wrappedComponent).to.be.instanceOf(TestComponent) + }) +}) + +describe('createwithFirestore', () => { + it('accepts a store key', () => { + createWithFirestore('store') + }) +}) diff --git a/tests/utils/components.js b/tests/utils/components.js new file mode 100644 index 000000000..78e5d7dcc --- /dev/null +++ b/tests/utils/components.js @@ -0,0 +1,60 @@ +import React, { Children, Component, cloneElement } from 'react' +import PropTypes from 'prop-types' +import ReactDOM from 'react-dom' +import TestUtils from 'react-addons-test-utils' +import { createSink } from 'recompose' +import { createStore, compose, combineReducers } from 'redux' +import reactReduxFirebase from '../../src/enhancer' +import { reduxFirestore } from 'redux-firestore' + +export class Passthrough extends Component { + render () { + return
+ } +} + +export class ProviderMock extends Component { + getChildContext () { + return { store: this.props.store } + } + + state = { test: null, dynamic: '' } + + render () { + return Children.only( + cloneElement(this.props.children, { + testProp: this.state.test, + dynamicProp: this.state.dynamic + })) + } +} + +ProviderMock.childContextTypes = { + store: PropTypes.object.isRequired +} + +ProviderMock.propTypes = { + store: PropTypes.object, + children: PropTypes.node +} + +export class TestContainer extends Component { + render () { + return + } +} + +export const storeWithFirebase = () => { + const createStoreWithMiddleware = compose( + reactReduxFirebase(Firebase, { userProfile: 'users' }) + )(createStore) + return createStoreWithMiddleware(combineReducers({ test: (state = {}) => state })) +} + +export const storeWithFirestore = () => { + const createStoreWithMiddleware = compose( + reactReduxFirebase(Firebase, { userProfile: 'users' }), + reduxFirestore(Firebase) + )(createStore) + return createStoreWithMiddleware(combineReducers({ test: (state = {}) => state })) +} From 472aa66103b4f5c44b0d4fdac3d5ce468f807b7e Mon Sep 17 00:00:00 2001 From: prescottprue Date: Tue, 12 Dec 2017 02:17:49 -0800 Subject: [PATCH 3/8] Fix double dispatch of FILE_UPLOAD_START when using progress: true option. package-lock updated. * dispatch added to withFirebase * Fix double dispatch of FILE_UPLOAD_START when using progress: true option * transform-decorators-legacy removed (no longer used in tests) --- .babelrc | 1 - package-lock.json | 345 +++++++++++++++++++++++- package.json | 1 - src/actions/storage.js | 3 +- src/withFirebase.js | 1 + src/withFirestore.js | 17 +- tests/unit/actions/storage.spec.js | 129 ++++++++- tests/unit/connect.spec.js | 130 --------- tests/unit/firebaseConnect.spec.js | 107 ++------ tests/unit/firestoreConnect.spec.js | 115 ++------ tests/unit/withFirebase.spec.js | 58 ++++ tests/unit/withFirestore.spec.js | 11 +- tests/{utils/components.js => utils.js} | 45 ++-- 13 files changed, 603 insertions(+), 360 deletions(-) delete mode 100644 tests/unit/connect.spec.js create mode 100644 tests/unit/withFirebase.spec.js rename tests/{utils/components.js => utils.js} (77%) diff --git a/.babelrc b/.babelrc index 8b5b98bc9..2b1c6803c 100644 --- a/.babelrc +++ b/.babelrc @@ -25,7 +25,6 @@ "test": { "plugins": [ "transform-runtime", - "transform-decorators-legacy", "transform-async-to-generator" ] } diff --git a/package-lock.json b/package-lock.json index 5ca5193ee..882769bbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-redux-firebase", - "version": "2.0.0-beta.19", + "version": "2.0.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1698,6 +1698,12 @@ } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, "boom": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", @@ -1980,6 +1986,16 @@ "check-error": "1.0.2" } }, + "chai-enzyme": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/chai-enzyme/-/chai-enzyme-0.8.0.tgz", + "integrity": "sha1-YJxVKh3NsJH0NeHigcxPIUmjO+E=", + "dev": true, + "requires": { + "html": "1.0.0", + "react-element-to-jsx-string": "5.0.7" + } + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -2028,6 +2044,30 @@ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, + "cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", + "dev": true, + "requires": { + "css-select": "1.2.0", + "dom-serializer": "0.1.0", + "entities": "1.1.1", + "htmlparser2": "3.9.2", + "lodash.assignin": "4.2.0", + "lodash.bind": "4.2.1", + "lodash.defaults": "4.2.0", + "lodash.filter": "4.6.0", + "lodash.flatten": "4.4.0", + "lodash.foreach": "4.5.0", + "lodash.map": "4.6.0", + "lodash.merge": "4.6.0", + "lodash.pick": "4.4.0", + "lodash.reduce": "4.6.0", + "lodash.reject": "4.6.0", + "lodash.some": "4.6.0" + } + }, "child-process-promise": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", @@ -2502,6 +2542,24 @@ "randomfill": "1.0.3" } }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "1.0.0", + "css-what": "2.1.0", + "domutils": "1.5.1", + "nth-check": "1.0.1" + } + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", + "dev": true + }, "cssom": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", @@ -3163,6 +3221,24 @@ } } }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + } + } + }, "dom-storage": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/dom-storage/-/dom-storage-2.0.2.tgz", @@ -3175,6 +3251,31 @@ "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", "dev": true }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", + "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", @@ -3212,6 +3313,12 @@ "jsbn": "0.1.1" } }, + "editions": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.3.tgz", + "integrity": "sha1-CQcQG92iD6w8vjNMJ8vQaI3Jmls=", + "dev": true + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3292,6 +3399,30 @@ "tapable": "0.2.8" } }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + }, + "enzyme": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-2.9.1.tgz", + "integrity": "sha1-B9XOaRJBJA+4F78sSxjW5TAkDfY=", + "dev": true, + "requires": { + "cheerio": "0.22.0", + "function.prototype.name": "1.0.3", + "is-subset": "0.1.1", + "lodash": "4.17.4", + "object-is": "1.0.1", + "object.assign": "4.0.4", + "object.entries": "1.0.4", + "object.values": "1.0.4", + "prop-types": "15.6.0", + "uuid": "3.1.0" + } + }, "errno": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", @@ -5382,6 +5513,17 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "function.prototype.name": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.0.3.tgz", + "integrity": "sha512-5EblxZUdioXi2JiMZ9FUbwYj40eQ9MFHyzFLBSPdlRl3SO8l7SLWuAnQ/at/1Wi4hjJwME/C5WpF2ZfAc8nGNw==", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "is-callable": "1.1.3" + } + }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -5997,6 +6139,15 @@ "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", "dev": true }, + "html": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/html/-/html-1.0.0.tgz", + "integrity": "sha1-pUT6nqVJK/s6LMqCEKEL57WvH2E=", + "dev": true, + "requires": { + "concat-stream": "1.6.0" + } + }, "html-encoding-sniffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", @@ -6012,6 +6163,20 @@ "integrity": "sha1-nSLgyjKsyVs/RbjVtPb73AWv/VU=", "dev": true }, + "htmlparser2": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", + "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", + "dev": true, + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.4.1", + "domutils": "1.5.1", + "entities": "1.1.1", + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, "http-errors": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", @@ -6454,6 +6619,12 @@ "has": "1.0.1" } }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", + "dev": true + }, "is-relative": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", @@ -6486,6 +6657,12 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, "is-symbol": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", @@ -7207,6 +7384,18 @@ "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", "dev": true }, + "lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=", + "dev": true + }, + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=", + "dev": true + }, "lodash.cond": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", @@ -7224,6 +7413,30 @@ "lodash._isiterateecall": "3.0.9" } }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, + "lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=", + "dev": true + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -7253,12 +7466,48 @@ "lodash.isarray": "3.0.4" } }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", + "integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU=", + "dev": true + }, "lodash.mergewith": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz", "integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=", "dev": true }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", + "dev": true + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=", + "dev": true + }, + "lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=", + "dev": true + }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", + "dev": true + }, "lolex": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", @@ -12460,6 +12709,15 @@ } } }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "dev": true, + "requires": { + "boolbase": "1.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -12524,6 +12782,12 @@ } } }, + "object-is": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", + "dev": true + }, "object-keys": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", @@ -12558,6 +12822,18 @@ "object-keys": "1.0.11" } }, + "object.entries": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", + "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0", + "function-bind": "1.1.1", + "has": "1.0.1" + } + }, "object.omit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", @@ -12585,6 +12861,18 @@ } } }, + "object.values": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", + "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0", + "function-bind": "1.1.1", + "has": "1.0.1" + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -13263,6 +13551,20 @@ "prop-types": "15.6.0" } }, + "react-element-to-jsx-string": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-5.0.7.tgz", + "integrity": "sha1-xmOkgAqccSEVwNhRnLAhWkah8PI=", + "dev": true, + "requires": { + "collapse-white-space": "1.0.3", + "is-plain-object": "2.0.4", + "lodash": "4.17.4", + "sortobject": "1.1.1", + "stringify-object": "2.4.0", + "traverse": "0.6.6" + } + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -13354,6 +13656,16 @@ "symbol-observable": "1.0.4" } }, + "redux-firestore": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/redux-firestore/-/redux-firestore-0.2.0.tgz", + "integrity": "sha512-hiN6KSmf11zx+IMtgsUWKOe5f8Q7n83EvAGsdJuoDaPc8/+MWCwdiXUci3Ij/RBmd+Kyozd/87htssS38RyzIA==", + "dev": true, + "requires": { + "lodash": "4.17.4", + "prop-types": "15.6.0" + } + }, "regenerate": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", @@ -13996,6 +14308,15 @@ "hoek": "2.16.3" } }, + "sortobject": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sortobject/-/sortobject-1.1.1.tgz", + "integrity": "sha1-T2ldTUTtCkwGSCw0wlgqLc3CqzQ=", + "dev": true, + "requires": { + "editions": "1.3.3" + } + }, "source-list-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", @@ -14284,6 +14605,16 @@ "is-hexadecimal": "1.0.1" } }, + "stringify-object": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-2.4.0.tgz", + "integrity": "sha1-xi0RAj6yH+LZsIe+A5om3zsioJ0=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0", + "is-regexp": "1.0.0" + } + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", @@ -14637,6 +14968,12 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", "dev": true }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "dev": true + }, "trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", @@ -15110,6 +15447,12 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", "dev": true }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "dev": true + }, "v8flags": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", diff --git a/package.json b/package.json index 043ff3afe..a19ff2f0c 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "babel-plugin-lodash": "^3.2.11", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-class-properties": "^6.23.0", - "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-object-assign": "^6.22.0", "babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0", diff --git a/src/actions/storage.js b/src/actions/storage.js index 05d4cda48..cce6a685e 100644 --- a/src/actions/storage.js +++ b/src/actions/storage.js @@ -22,8 +22,7 @@ const { * @param {Blob} opts.file - File to upload * @private */ -export const uploadFileWithProgress = (dispatch, firebase, { path, file }) => { - dispatch({ type: FILE_UPLOAD_START, payload: { path, file } }) +const uploadFileWithProgress = (dispatch, firebase, { path, file }) => { const uploadEvent = firebase.storage().ref(`${path}/${file.name}`).put(file) // TODO: Allow config to control whether progress it set to state or not const unListen = uploadEvent.on( diff --git a/src/withFirebase.js b/src/withFirebase.js index 4667c1444..566125e2e 100644 --- a/src/withFirebase.js +++ b/src/withFirebase.js @@ -37,6 +37,7 @@ export const createWithFirebase = (storeKey = 'store') => WrappedComponent => { ) diff --git a/src/withFirestore.js b/src/withFirestore.js index 20b94c6cc..25c2f5149 100644 --- a/src/withFirestore.js +++ b/src/withFirestore.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import hoistStatics from 'hoist-non-react-statics' import { wrapDisplayName } from 'recompose' + /** * @name createWithFirestore * @description Function that creates a Higher Order Component that @@ -12,11 +13,10 @@ import { wrapDisplayName } from 'recompose' * Firebase state (state.firebase) * @return {Function} - HOC that accepts a watchArray and wraps a component * @example Basic - * // this.props.firebase set on App component as firebase object with helpers - * import { createWithFirebase } from 'react-redux-firebase' + * import { createWithFirestore } from 'react-redux-firebase' * * // create withFirestore that uses another redux store - * const withFirestore = createWithFirebase('anotherStore') + * const withFirestore = createWithFirestore('anotherStore') * * // use the withFirestore to wrap a component * export default withFirestore(SomeComponent) @@ -35,10 +35,9 @@ export const createWithFirestore = (storeKey = 'store') => WrappedComponent => { return ( ) } @@ -52,15 +51,15 @@ export const createWithFirestore = (storeKey = 'store') => WrappedComponent => { * @extends React.Component * @description Higher Order Component that attaches props.firestore and * props.firebase to React Components. - * Firebase is gathered from store.firebase, which is attached to store by - * the store enhancer (reactReduxFirebase) in ./enhancer. + * Firebase is gathered from store.firestore, which is attached to store by + * the store enhancer (reduxFirestore) in ./enhancer. * @return {Function} - That accepts a component to wrap and returns the wrapped component * @example Basic * import { withFirestore } from 'react-redux-firebase' * - * const AddData = ({ firebase: { push } }) => + * const AddData = ({ firestore: { add } }) => *
- * *
diff --git a/tests/unit/actions/storage.spec.js b/tests/unit/actions/storage.spec.js index d05f0d1f5..51b3d6cdc 100644 --- a/tests/unit/actions/storage.spec.js +++ b/tests/unit/actions/storage.spec.js @@ -8,6 +8,8 @@ import { let spy let unListen = sinon.spy() const dispatch = () => {} +const defaultFileMeta = { path: 'projects', file: { name: 'test.png' } } +let dispatchSpy const fakeFirebase = { _: { authUid: '123', @@ -74,19 +76,122 @@ describe('Actions: Storage', () => { expect(uploadFile).to.be.a.function }) - it('runs given basic params', () => - expect(uploadFile(dispatch, fakeFirebase, { path: 'projects', file: { name: 'test.png' } })) - .to - .eventually - .become({}) - ) + it('throws if storage does not exist', async () => { + expect(() => uploadFile(dispatch, {})).to.Throw('Firebase storage is required to upload files') + }) - it('handles dbPath', () => - expect(uploadFile(dispatch, fakeFirebase, { path: 'projects', file: { name: 'test.png' }, dbPath: 'projects' })) - .to - .eventually - .become({}) - ) + it('runs given basic params', async () => { + const putSpy = sinon.spy(() => Promise.resolve({})) + const fake = { + storage: () => ({ ref: () => ({ put: putSpy }) }), + _: firebase._ + } + await uploadFile(spy, fake, { path: 'projects', file: { name: 'test.png' } }) + // firebase.storage() put method is called + expect(putSpy).to.have.been.calledOnce + // dispatch is called twice (once for FILE_UPLOAD_START, the other for FILE_UPLOAD_COMPLETE) + expect(spy).to.have.been.calledTwice + }) + + it('skips straight to dispatch if firebase.database does not exist when dbPath is included', async () => { + const putSpy = sinon.spy(() => Promise.resolve({})) + const fake = { + storage: () => ({ ref: () => ({ put: putSpy }) }), + _: firebase._ + } + await uploadFile(spy, fake, { ...defaultFileMeta, dbPath: 'test' }) + // firebase.storage() put method is called + expect(putSpy).to.have.been.calledOnce + // dispatch is called twice (once for FILE_UPLOAD_START, the other for FILE_UPLOAD_COMPLETE) + expect(spy).to.have.been.calledTwice + }) + + it('handles dbPath', async () => { + const res = await uploadFile(dispatch, fakeFirebase, { ...defaultFileMeta, dbPath: 'projects' }) + expect(res).to.be.an.object + }) + + it('calls database.push', async () => { + const putSpy = sinon.spy(() => Promise.resolve({ + metadata: { + name: 'file.png', + fullPath: 'test', + downloadURLs: [{ path: 'asdf' }] + } + })) + const pushSpy = sinon.spy(() => Promise.resolve({})) + const fake = { + storage: () => ({ ref: () => ({ put: putSpy }) }), + database: Object.assign( + () => ({ ref: () => ({ push: pushSpy }) }), + { ServerValue: { TIMESTAMP: 123 } } + ), + _: firebase._ + } + await uploadFile(spy, fake, { ...defaultFileMeta, dbPath: 'test' }) + // firebase.storage() put method is called + expect(putSpy).to.have.been.calledOnce + // firebase.storage() put method is called + expect(pushSpy).to.have.been.calledOnce + // dispatch is called twice (once for FILE_UPLOAD_START, the other for FILE_UPLOAD_COMPLETE) + expect(spy).to.have.been.calledTwice + }) + + it('dispatches for errors and rejects', async () => { + const putSpy = sinon.spy(() => Promise.reject(new Error('Test'))) + const fake = { + storage: () => ({ ref: () => ({ put: putSpy }) }), + _: firebase._ + } + try { + await uploadFile(spy, fake, { ...defaultFileMeta, dbPath: 'test' }) + } catch (err) { + expect(err.message).to.equal('Test') + } + // firebase.storage() put method is called + expect(putSpy).to.have.been.calledOnce + // dispatch is called twice (once for FILE_UPLOAD_START, the other for FILE_UPLOAD_COMPLETE) + expect(spy).to.have.been.calledTwice + }) + + describe('options', () => { + describe('name option', () => { + it('renames file given a string', async () => { + const options = { name: 'newname.png' } + await uploadFile(spy, fakeFirebase, { ...defaultFileMeta, options }) + // dispatch is called + // TODO: Confirm dispatch is called with correct name + expect(spy).to.have.been.calledOnce + }) + + it('renames file given a function', async () => { + const nameFunc = sinon.spy() + const options = { name: nameFunc } + await uploadFile(spy, fakeFirebase, { ...defaultFileMeta, options }) + // firebase.storage() put method is called + expect(nameFunc).to.have.been.calledOnce + }) + }) + + describe('progress option', () => { + it('calls uploadFileWithProgress', async () => { + const putSpy = sinon.spy(() => Object.assign(Promise.resolve({}), { on: sinon.spy() })) + const fake = { + storage: Object.assign(() => ({ + ref: () => ({ put: putSpy }) }), + { TaskEvent: { STATE_CHANGED: 'STATE_CHANGED' } + }), + _: firebase._ + } + const options = { progress: true } + await uploadFile(spy, fake, { ...defaultFileMeta, options }) + // firebase.storage() put method is called + expect(putSpy).to.have.been.calledOnce + // dispatch is called twice (once for FILE_UPLOAD_START, next for FILE_UPLOAD_COMPLETE,) + expect(spy).to.have.been.calledTwice + }) + }) + }) }) describe('uploadFiles', () => { diff --git a/tests/unit/connect.spec.js b/tests/unit/connect.spec.js deleted file mode 100644 index 196624b68..000000000 --- a/tests/unit/connect.spec.js +++ /dev/null @@ -1,130 +0,0 @@ -import React, { Children, Component, cloneElement } from 'react' -import PropTypes from 'prop-types' -import ReactDOM from 'react-dom' -import TestUtils from 'react-addons-test-utils' -import firebaseConnect from '../../src/firebaseConnect' -import reactReduxFirebase from '../../src/enhancer' -import { createStore, compose, combineReducers } from 'redux' - -describe('firebaseConnect', () => { - class Passthrough extends Component { - render () { - return
{JSON.stringify(this.props)}
- } - } - - class ProviderMock extends Component { - getChildContext () { - return { store: this.props.store } - } - - state = { test: null, dynamic: '' } - - render () { - return Children.only( - cloneElement(this.props.children, - { testProp: this.state.test, dynamicProp: this.state.dynamic } - )) - } - } - - ProviderMock.childContextTypes = { - store: PropTypes.object.isRequired - } - - ProviderMock.propTypes = { - children: PropTypes.node, - store: PropTypes.object.isRequired - } - - const createContainer = () => { - const createStoreWithMiddleware = compose( - reactReduxFirebase(Firebase, { userProfile: 'users' }), - typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : f => f - )(createStore) - const store = createStoreWithMiddleware(combineReducers({ test: (state = {}) => state })) - - @firebaseConnect((props) => [ - `test/${props.dynamicProp}` - ]) - class Container extends Component { - render () { - return - } - } - - const tree = TestUtils.renderIntoDocument( - - - - ) - - return { - container: TestUtils.findRenderedComponentWithType(tree, Container), - parent: TestUtils.findRenderedComponentWithType(tree, ProviderMock), - store - } - } - - it('should receive the store in the context', () => { - const { container, store } = createContainer() - expect(container.context.store).to.equal(store) - }) - - it('disables watchers on unmount', () => { - const { container, store } = createContainer() - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(container).parentNode) - expect(container.context.store).to.equal(store) - }) - - it('does not change watchers props changes that do not change listener paths', () => { - const { parent } = createContainer() - parent.setState({ test: 'somethingElse' }) - // expect(parent.context.store).to.equal(store) - }) - - it('reapplies watchers when props change', () => { - const { parent } = createContainer() - parent.setState({ - dynamic: 'somethingElse' - }) - // expect(parent.context.store).to.equal(store) - }) - - describe('sets displayName static as ', () => { - describe('FirebaseConnect(${WrappedComponentName}) for', () => { // eslint-disable-line no-template-curly-in-string - it('standard components', () => { - class TestContainer extends Component { - render () { - return - } - } - - const containerPrime = firebaseConnect()(TestContainer) - expect(containerPrime.displayName).to.equal(`FirebaseConnect(TestContainer)`) - }) - - it('string components', () => { - const str = 'Test' - const stringComp = firebaseConnect()('Test') - expect(stringComp.displayName).to.equal(`FirebaseConnect(${str})`) - }) - }) - - it('"Component" for all other types', () => { - const stringComp = firebaseConnect()(
) - expect(stringComp.displayName).to.equal('FirebaseConnect(Component)') - }) - }) - - it('sets WrappedComponent static as component which was wrapped', () => { - class Container extends Component { - render () { - return - } - } - - const containerPrime = firebaseConnect()(Container) - expect(containerPrime.wrappedComponent).to.equal(Container) - }) -}) diff --git a/tests/unit/firebaseConnect.spec.js b/tests/unit/firebaseConnect.spec.js index e0c29f5ed..bc1301139 100644 --- a/tests/unit/firebaseConnect.spec.js +++ b/tests/unit/firebaseConnect.spec.js @@ -1,72 +1,30 @@ -import React, { Children, Component, cloneElement } from 'react' -import PropTypes from 'prop-types' +import React from 'react' import ReactDOM from 'react-dom' import TestUtils from 'react-addons-test-utils' -import { createStore, compose, combineReducers } from 'redux' -import reactReduxFirebase from '../../src/enhancer' +import { createSink } from 'recompose' +import { storeWithFirebase, Container, ProviderMock, TestContainer } from '../utils' import firebaseConnect, { createFirebaseConnect } from '../../src/firebaseConnect' -describe('firebaseConnect', () => { - class Passthrough extends Component { - render () { - return
{JSON.stringify(this.props)}
- } - } - - class ProviderMock extends Component { - getChildContext () { - return { store: this.props.store } - } - - state = { test: null, dynamic: '' } - - render () { - return Children.only( - cloneElement(this.props.children, { - testProp: this.state.test, - dynamicProp: this.state.dynamic - })) - } - } - - ProviderMock.childContextTypes = { - store: PropTypes.object.isRequired - } - - ProviderMock.propTypes = { - store: PropTypes.object, - children: PropTypes.node - } - - const createContainer = () => { - const createStoreWithMiddleware = compose( - reactReduxFirebase(Firebase, { userProfile: 'users' }), - typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : f => f - )(createStore) - const store = createStoreWithMiddleware(combineReducers({ test: (state = {}) => state })) - - @firebaseConnect((props) => [ - `test/${props.dynamicProp}` - ]) - class Container extends Component { - render () { - return - } - } - - const tree = TestUtils.renderIntoDocument( - - - - ) - - return { - container: TestUtils.findRenderedComponentWithType(tree, Container), - parent: TestUtils.findRenderedComponentWithType(tree, ProviderMock), - store - } +const createContainer = () => { + const store = storeWithFirebase() + const WrappedContainer = firebaseConnect((props) => [ + `test/${props.dynamicProp}` + ])(Container) + + const tree = TestUtils.renderIntoDocument( + + + + ) + + return { + container: TestUtils.findRenderedComponentWithType(tree, WrappedContainer), + parent: TestUtils.findRenderedComponentWithType(tree, ProviderMock), + store } +} +describe('firebaseConnect', () => { it('should receive the store in the context', () => { const { container, store } = createContainer() expect(container.context.store).to.equal(store) @@ -94,13 +52,7 @@ describe('firebaseConnect', () => { describe('sets displayName static as ', () => { describe('FirebaseConnect(${WrappedComponentName}) for', () => { // eslint-disable-line no-template-curly-in-string - it('standard components', () => { - class TestContainer extends Component { - render () { - return - } - } - + it('class components', () => { const containerPrime = firebaseConnect()(TestContainer) expect(containerPrime.displayName).to.equal(`FirebaseConnect(TestContainer)`) }) @@ -119,17 +71,14 @@ describe('firebaseConnect', () => { }) it('sets WrappedComponent static as component which was wrapped', () => { - class Container extends Component { - render () { - return - } - } - - const containerPrime = firebaseConnect()(Container) - expect(containerPrime.wrappedComponent).to.equal(Container) + const component = createSink() + const containerPrime = firebaseConnect()(component) + expect(containerPrime.wrappedComponent).to.equal(component) }) }) describe('createFirebaseConnect', () => { - createFirebaseConnect('store') + it('accepts a different store key', () => { + createFirebaseConnect('store2') + }) }) diff --git a/tests/unit/firestoreConnect.spec.js b/tests/unit/firestoreConnect.spec.js index 2a3364878..b62432c4c 100644 --- a/tests/unit/firestoreConnect.spec.js +++ b/tests/unit/firestoreConnect.spec.js @@ -1,84 +1,31 @@ -import React, { Children, Component, cloneElement } from 'react' -import PropTypes from 'prop-types' +import React from 'react' import ReactDOM from 'react-dom' import TestUtils from 'react-addons-test-utils' -import reactReduxFirebase from '../../src/enhancer' -import { createStore, compose, combineReducers } from 'redux' +import { storeWithFirestore, Container, ProviderMock, TestContainer } from '../utils' import firestoreConnect, { createFirestoreConnect } from '../../src/firestoreConnect' -describe('firestoreConnect', () => { - class Passthrough extends Component { - render () { - return
{JSON.stringify('yes')}
- } - } - - class ProviderMock extends Component { - getChildContext () { - return { store: this.props.store } - } - - state = { test: null, dynamic: '' } - - render () { - return Children.only( - cloneElement(this.props.children, { - testProp: this.state.test, - dynamicProp: this.state.dynamic - }) - ) - } - } - - ProviderMock.childContextTypes = { - store: PropTypes.object.isRequired - } - - ProviderMock.propTypes = { - store: PropTypes.object, - children: PropTypes.node - } - const fakeReduxFirestore = (instance, otherConfig) => next => ( - reducer, - initialState, - middleware - ) => { - const store = next(reducer, initialState, middleware) - store.firestore = { listeners: {} } - return store - } - - const createContainer = () => { - const createStoreWithMiddleware = compose( - reactReduxFirebase(Firebase, { userProfile: 'users' }), - fakeReduxFirestore(Firebase) - )(createStore) - const store = createStoreWithMiddleware( - combineReducers({ firestore: (state = {}) => state }) - ) - - @firestoreConnect(props => [`test/${props.dynamicProp}`]) - class Container extends Component { - render () { - return - } - } - - const tree = TestUtils.renderIntoDocument( - - - - ) - - return { - container: TestUtils.findRenderedComponentWithType(tree, Container), - parent: TestUtils.findRenderedComponentWithType(tree, ProviderMock), - store - } +const createContainer = () => { + const store = storeWithFirestore() + const WrappedContainer = firestoreConnect((props) => [ + `test/${props.dynamicProp}` + ])(Container) + + const tree = TestUtils.renderIntoDocument( + + + + ) + + return { + container: TestUtils.findRenderedComponentWithType(tree, WrappedContainer), + parent: TestUtils.findRenderedComponentWithType(tree, ProviderMock), + store } +} +describe('firestoreConnect', () => { it('should receive the store in the context', () => { const { container, store } = createContainer() expect(container.context.store).to.equal(store) @@ -98,21 +45,13 @@ describe('firestoreConnect', () => { it('reapplies watchers when props change', () => { const { container, store } = createContainer() - container.setState({ - dynamic: 'somethingElse' - }) + container.setState({ dynamic: 'somethingElse' }) expect(container.context.store).to.equal(store) }) describe('sets displayName static as ', () => { describe('FirestoreConnect(${WrappedComponentName}) for', () => { // eslint-disable-line no-template-curly-in-string it('standard components', () => { - class TestContainer extends Component { - render () { - return - } - } - const containerPrime = firestoreConnect()(TestContainer) expect(containerPrime.displayName).to.equal( `FirestoreConnect(TestContainer)` @@ -133,19 +72,13 @@ describe('firestoreConnect', () => { }) it('sets WrappedComponent static as component which was wrapped', () => { - class Container extends Component { - render () { - return - } - } - - const containerPrime = firestoreConnect()(Container) - expect(containerPrime.wrappedComponent).to.equal(Container) + const containerPrime = firestoreConnect()(TestContainer) + expect(containerPrime.wrappedComponent).to.equal(TestContainer) }) }) describe('createFirestoreConnect', () => { it('creates a function', () => { - expect(createFirestoreConnect('store')).to.be.a.function + expect(createFirestoreConnect('store2')).to.be.a.function }) }) diff --git a/tests/unit/withFirebase.spec.js b/tests/unit/withFirebase.spec.js new file mode 100644 index 000000000..c849b5012 --- /dev/null +++ b/tests/unit/withFirebase.spec.js @@ -0,0 +1,58 @@ +import React from 'react' +import { createSink } from 'recompose' +import { shallow } from 'enzyme' +import { storeWithFirebase } from '../utils' +import withFirebase, { createWithFirebase } from '../../src/withFirebase' + +let store +let TestComponent +let wrapper + +describe('withFirebase', () => { + beforeEach(() => { + store = storeWithFirebase() + TestComponent = withFirebase(createSink) + wrapper = shallow(, { context: { store } }) + }) + + it('adds firebase as prop', () => { + expect(wrapper.prop('firebase')).to.exist + expect(wrapper.prop('firebase')).to.respondTo('push') + }) + + it('adds dispatch as prop', () => { + expect(wrapper.prop('dispatch')).to.exist + expect(wrapper.prop('dispatch')).to.be.a.function + }) + + describe('sets displayName static as', () => { + describe('withFirebase(${WrappedComponentName}) for', () => { // eslint-disable-line no-template-curly-in-string + it.skip('standard components', () => { + wrapper = shallow(, { context: { store } }) + expect(wrapper.instance.displayName).to.equal(`withFirebase(TestContainer)`) + }) + + it('string components', () => { + const str = 'Test' + const stringComp = withFirebase(str) + expect(stringComp.displayName).to.equal(`withFirebase(${str})`) + }) + }) + + it.skip('"Component" for all other types', () => { + wrapper = shallow(withFirebase()(
)) + expect(wrapper.displayName).to.equal('withFirebase(Component)') + }) + }) + + it.skip('sets WrappedComponent static as component which was wrapped', () => { + wrapper = shallow(, { context: { store } }) + expect(wrapper.wrappedComponent).to.be.instanceOf(TestComponent) + }) +}) + +describe('createwithFirebase', () => { + it('accepts a different store key', () => { + createWithFirebase('store2') + }) +}) diff --git a/tests/unit/withFirestore.spec.js b/tests/unit/withFirestore.spec.js index fd0fef676..86dde83a9 100644 --- a/tests/unit/withFirestore.spec.js +++ b/tests/unit/withFirestore.spec.js @@ -1,8 +1,7 @@ -import React, { createElement } from 'react' -import TestUtils from 'react-addons-test-utils' -import { createSink, nest, renderComponent } from 'recompose' -import {mount, render, shallow} from 'enzyme' -import { TestContainer, ProviderMock, storeWithFirestore } from '../utils/components' +import React from 'react' +import { createSink } from 'recompose' +import { shallow } from 'enzyme' +import { storeWithFirestore } from '../utils' import withFirestore, { createWithFirestore } from '../../src/withFirestore' let store @@ -59,6 +58,6 @@ describe('withFirestore', () => { describe('createwithFirestore', () => { it('accepts a store key', () => { - createWithFirestore('store') + createWithFirestore('store2') }) }) diff --git a/tests/utils/components.js b/tests/utils.js similarity index 77% rename from tests/utils/components.js rename to tests/utils.js index 78e5d7dcc..3e26ec990 100644 --- a/tests/utils/components.js +++ b/tests/utils.js @@ -1,18 +1,28 @@ import React, { Children, Component, cloneElement } from 'react' import PropTypes from 'prop-types' -import ReactDOM from 'react-dom' -import TestUtils from 'react-addons-test-utils' import { createSink } from 'recompose' import { createStore, compose, combineReducers } from 'redux' -import reactReduxFirebase from '../../src/enhancer' import { reduxFirestore } from 'redux-firestore' +import reactReduxFirebase from '../src/enhancer' -export class Passthrough extends Component { - render () { - return
- } +export const storeWithFirebase = () => { + const createStoreWithMiddleware = compose( + reactReduxFirebase(Firebase, { userProfile: 'users' }) + )(createStore) + return createStoreWithMiddleware(combineReducers({ test: (state = {}) => state })) } +export const storeWithFirestore = () => { + const createStoreWithMiddleware = compose( + reactReduxFirebase(Firebase, { userProfile: 'users' }), + reduxFirestore(Firebase) // mock for reduxFirestore from redux-firestore + )(createStore) + return createStoreWithMiddleware(combineReducers({ test: (state = {}) => state })) +} + +export const TestContainer = () => createSink() +export const Container = () =>
+ export class ProviderMock extends Component { getChildContext () { return { store: this.props.store } @@ -37,24 +47,3 @@ ProviderMock.propTypes = { store: PropTypes.object, children: PropTypes.node } - -export class TestContainer extends Component { - render () { - return - } -} - -export const storeWithFirebase = () => { - const createStoreWithMiddleware = compose( - reactReduxFirebase(Firebase, { userProfile: 'users' }) - )(createStore) - return createStoreWithMiddleware(combineReducers({ test: (state = {}) => state })) -} - -export const storeWithFirestore = () => { - const createStoreWithMiddleware = compose( - reactReduxFirebase(Firebase, { userProfile: 'users' }), - reduxFirestore(Firebase) - )(createStore) - return createStoreWithMiddleware(combineReducers({ test: (state = {}) => state })) -} From 404c7c79f1e5952f79893cae911a4cee4863ef29 Mon Sep 17 00:00:00 2001 From: prescottprue Date: Tue, 12 Dec 2017 02:41:12 -0800 Subject: [PATCH 4/8] A few more test updates. --- tests/unit/actions/storage.spec.js | 1 - tests/unit/firestoreConnect.spec.js | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/unit/actions/storage.spec.js b/tests/unit/actions/storage.spec.js index 51b3d6cdc..d500579ad 100644 --- a/tests/unit/actions/storage.spec.js +++ b/tests/unit/actions/storage.spec.js @@ -9,7 +9,6 @@ let spy let unListen = sinon.spy() const dispatch = () => {} const defaultFileMeta = { path: 'projects', file: { name: 'test.png' } } -let dispatchSpy const fakeFirebase = { _: { authUid: '123', diff --git a/tests/unit/firestoreConnect.spec.js b/tests/unit/firestoreConnect.spec.js index b62432c4c..b59f51e23 100644 --- a/tests/unit/firestoreConnect.spec.js +++ b/tests/unit/firestoreConnect.spec.js @@ -26,6 +26,11 @@ const createContainer = () => { } describe('firestoreConnect', () => { + it('should render if Firestore does not exist', () => { + const { container } = createContainer() + // TODO: Pass a fake store through context + expect(container).to.exist + }) it('should receive the store in the context', () => { const { container, store } = createContainer() expect(container.context.store).to.equal(store) @@ -37,16 +42,16 @@ describe('firestoreConnect', () => { expect(container.context.store).to.equal(store) }) - it('does not change watchers props changes that do not change listener paths', () => { - const { container, store } = createContainer() - container.setState({ test: 'somethingElse' }) - expect(container.context.store).to.equal(store) + it('does not change watchers with props changes that do not change listener paths', () => { + const { parent, container } = createContainer() + parent.setState({ test: 'somethingElse' }) + expect(container.prevData).to.be.null }) it('reapplies watchers when props change', () => { - const { container, store } = createContainer() - container.setState({ dynamic: 'somethingElse' }) - expect(container.context.store).to.equal(store) + const { parent, container } = createContainer() + parent.setState({ dynamic: 'somethingElse' }) + expect(container.prevData).to.be.null }) describe('sets displayName static as ', () => { From df31841dd11dbed1795e76957faa58b5735ce4b8 Mon Sep 17 00:00:00 2001 From: prescottprue Date: Wed, 13 Dec 2017 23:54:08 -0800 Subject: [PATCH 5/8] withFirestore and withFirebase docs updated --- docs/api/withFirebase.md | 48 ++++++++++++++++++++++----- docs/api/withFirestore.md | 69 +++++++++++++++++++++++++++------------ src/withFirebase.js | 43 ++++++++++++++++++++---- src/withFirestore.js | 46 ++++++++++++++++++++------ 4 files changed, 161 insertions(+), 45 deletions(-) diff --git a/docs/api/withFirebase.md b/docs/api/withFirebase.md index 2f464ab50..15eaf65e3 100644 --- a/docs/api/withFirebase.md +++ b/docs/api/withFirebase.md @@ -8,15 +8,15 @@ ## createWithFirebase Function that creates a Higher Order Component that -automatically listens/unListens to provided firebase paths using -React's Lifecycle hooks. +which provides `firebase` and `dispatch` as a props to React Components. + **WARNING!!** This is an advanced feature, and should only be used when needing to access a firebase instance created under a different store key. **Parameters** - `storeKey` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Name of redux store which contains - Firebase state (state.firebase) (optional, default `'store'`) + Firebase state (`state.firebase`) (optional, default `'store'`) **Examples** @@ -33,15 +33,20 @@ const withFirebase = createWithFirebase('anotherStore') export default withFirebase(SomeComponent) ``` -Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** HOC that accepts a watchArray and wraps a component +Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** Higher Order Component which accepts an array of +watchers config and wraps a React Component ## withFirebase **Extends React.Component** -Higher Order Component that attaches firebase to props. -Firebase is gathered from store.firebase, which is attached to store by -the store enhancer (reactReduxFirebase) in ./enhancer. +Higher Order Component that provides `firebase` and +`dispatch` as a props to React Components. Firebase is gathered from +`store.firebase`, which is attached to store by the store enhancer +(`reactReduxFirebase`) during setup. +**NOTE**: This version of the Firebase library has extra methods, config, +and functionality which give it it's capabilities such as dispatching +actions. **Examples** @@ -60,4 +65,31 @@ const AddData = ({ firebase: { push } }) => export default withFirebase(AddData) ``` -Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** That accepts a component to wrap and returns the wrapped component +_Within HOC Composition_ + +```javascript +import { compose } from 'redux' // can also come from recompose +import { withHandlers } from 'recompose' +import { withFirebase } from 'react-redux-firebase' + +const AddTodo = ({ addTodo }) => +
+ +
+ +export default compose( + withFirebase(AddTodo), + withHandlers({ + addTodo: props => () => + props.firestore.add( + { collection: 'todos' }, + { done: false, text: 'Sample' } + ) + }) +) +``` + +Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** Which accepts a component to wrap and returns the +wrapped component diff --git a/docs/api/withFirestore.md b/docs/api/withFirestore.md index 1c97919c8..26a141d03 100644 --- a/docs/api/withFirestore.md +++ b/docs/api/withFirestore.md @@ -3,61 +3,90 @@ ### Table of Contents - [createWithFirestore](#createwithfirestore) -- [withFirebase](#withfirebase) +- [withFirestore](#withfirestore) ## createWithFirestore Function that creates a Higher Order Component that -automatically listens/unListens to provided firebase paths using -React's Lifecycle hooks. +which provides `firebase`, `firestore`, and `dispatch` to React Components. + **WARNING!!** This is an advanced feature, and should only be used when needing to access a firebase instance created under a different store key. **Parameters** - `storeKey` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Name of redux store which contains - Firebase state (state.firebase) (optional, default `'store'`) + Firebase state (`state.firebase`) (optional, default `'store'`) **Examples** _Basic_ ```javascript -// this.props.firebase set on App component as firebase object with helpers -import { createWithFirebase } from 'react-redux-firebase' +import { createWithFirestore } from 'react-redux-firebase' -// create withFirebase that uses another redux store -const withFirebase = createWithFirebase('anotherStore') +// create withFirestore that uses another redux store +const withFirestore = createWithFirestore('anotherStore') -// use the withFirebase to wrap a component -export default withFirebase(SomeComponent) +// use the withFirestore to wrap a component +export default withFirestore(SomeComponent) ``` -Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** HOC that accepts a watchArray and wraps a component +Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** Higher Order Component which accepts an array of +watchers config and wraps a React Component -## withFirebase +## withFirestore **Extends React.Component** -Higher Order Component that attaches firebase to props. -Firebase is gathered from store.firebase, which is attached to store by -the store enhancer (reactReduxFirebase) in ./enhancer. +Higher Order Component that attaches `firestore`, `firebase` +and `dispatch` as props to React Components. Firebase instance is gathered +from `store.firestore`, which is attached to store by the store enhancer +(`reduxFirestore`) during setup of +[`redux-firestore`](https://github.com/prescottprue/redux-firestore) **Examples** _Basic_ ```javascript -import { withFirebase } from 'react-redux-firebase' +import { withFirestore } from 'react-redux-firebase' + +const AddTodo = ({ firestore: { add } }) => +
+ +
+ +export default withFirestore(AddTodo) +``` + +_Within HOC Composition_ + +```javascript +import { compose } from 'redux' // can also come from recompose +import { withHandlers } from 'recompose' +import { withFirestore } from 'react-redux-firebase' -const AddData = ({ firebase: { push } }) => +const AddTodo = ({ addTodo }) =>
-
-export default withFirebase(AddData) +export default compose( + withFirestore(AddTodo), + withHandlers({ + addTodo: props => () => + props.firestore.add( + { collection: 'todos' }, + { done: false, text: 'Sample' } + ) + }) +) ``` -Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** That accepts a component to wrap and returns the wrapped component +Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** Which accepts a component to wrap and returns the +wrapped component diff --git a/src/withFirebase.js b/src/withFirebase.js index 566125e2e..a43d9096f 100644 --- a/src/withFirebase.js +++ b/src/withFirebase.js @@ -6,12 +6,14 @@ import { wrapDisplayName } from 'recompose' /** * @name createWithFirebase * @description Function that creates a Higher Order Component that - * which providers props.firebase to React Components. + * which provides `firebase` and `dispatch` as a props to React Components. + * * **WARNING!!** This is an advanced feature, and should only be used when * needing to access a firebase instance created under a different store key. * @param {String} [storeKey='store'] - Name of redux store which contains - * Firebase state (state.firebase) - * @return {Function} - HOC that accepts a watchArray and wraps a component + * Firebase state (`state.firebase`) + * @return {Function} - Higher Order Component which accepts an array of + * watchers config and wraps a React Component * @example Basic * // this.props.firebase set on App component as firebase object with helpers * import { createWithFirebase } from 'react-redux-firebase' @@ -50,10 +52,15 @@ export const createWithFirebase = (storeKey = 'store') => WrappedComponent => { /** * @name withFirebase * @extends React.Component - * @description Higher Order Component that attaches firebase to props. - * Firebase is gathered from store.firebase, which is attached to store by - * the store enhancer (reactReduxFirebase) in ./enhancer. - * @return {Function} - That accepts a component to wrap and returns the wrapped component + * @description Higher Order Component that provides `firebase` and + * `dispatch` as a props to React Components. Firebase is gathered from + * `store.firebase`, which is attached to store by the store enhancer + * (`reactReduxFirebase`) during setup. + * **NOTE**: This version of the Firebase library has extra methods, config, + * and functionality which give it it's capabilities such as dispatching + * actions. + * @return {Function} - Which accepts a component to wrap and returns the + * wrapped component * @example Basic * import { withFirebase } from 'react-redux-firebase' * @@ -65,5 +72,27 @@ export const createWithFirebase = (storeKey = 'store') => WrappedComponent => { *
* * export default withFirebase(AddData) + * @example Within HOC Composition + * import { compose } from 'redux' // can also come from recompose + * import { withHandlers } from 'recompose' + * import { withFirebase } from 'react-redux-firebase' + * + * const AddTodo = ({ addTodo }) => + *
+ * + *
+ * + * export default compose( + * withFirebase(AddTodo), + * withHandlers({ + * addTodo: props => () => + * props.firestore.add( + * { collection: 'todos' }, + * { done: false, text: 'Sample' } + * ) + * }) + * ) */ export default createWithFirebase() diff --git a/src/withFirestore.js b/src/withFirestore.js index 25c2f5149..5df737135 100644 --- a/src/withFirestore.js +++ b/src/withFirestore.js @@ -6,12 +6,14 @@ import { wrapDisplayName } from 'recompose' /** * @name createWithFirestore * @description Function that creates a Higher Order Component that - * which providers props.firestore and props.firebase to React Components. + * which provides `firebase`, `firestore`, and `dispatch` to React Components. + * * **WARNING!!** This is an advanced feature, and should only be used when * needing to access a firebase instance created under a different store key. * @param {String} [storeKey='store'] - Name of redux store which contains - * Firebase state (state.firebase) - * @return {Function} - HOC that accepts a watchArray and wraps a component + * Firebase state (`state.firebase`) + * @return {Function} - Higher Order Component which accepts an array of + * watchers config and wraps a React Component * @example Basic * import { createWithFirestore } from 'react-redux-firebase' * @@ -49,21 +51,45 @@ export const createWithFirestore = (storeKey = 'store') => WrappedComponent => { /** * @name withFirestore * @extends React.Component - * @description Higher Order Component that attaches props.firestore and - * props.firebase to React Components. - * Firebase is gathered from store.firestore, which is attached to store by - * the store enhancer (reduxFirestore) in ./enhancer. - * @return {Function} - That accepts a component to wrap and returns the wrapped component + * @description Higher Order Component that attaches `firestore`, `firebase` + * and `dispatch` as props to React Components. Firebase instance is gathered + * from `store.firestore`, which is attached to store by the store enhancer + * (`reduxFirestore`) during setup of + * [`redux-firestore`](https://github.com/prescottprue/redux-firestore) + * @return {Function} - Which accepts a component to wrap and returns the + * wrapped component * @example Basic * import { withFirestore } from 'react-redux-firebase' * - * const AddData = ({ firestore: { add } }) => + * const AddTodo = ({ firestore: { add } }) => *
* *
* - * export default withFirestore(AddData) + * export default withFirestore(AddTodo) + * @example Within HOC Composition + * import { compose } from 'redux' // can also come from recompose + * import { withHandlers } from 'recompose' + * import { withFirestore } from 'react-redux-firebase' + * + * const AddTodo = ({ addTodo }) => + *
+ * + *
+ * + * export default compose( + * withFirestore(AddTodo), + * withHandlers({ + * addTodo: props => () => + * props.firestore.add( + * { collection: 'todos' }, + * { done: false, text: 'Sample' } + * ) + * }) + * ) */ export default createWithFirestore() From ea28179ae05001fd6d8613d43e2290f3b3962439 Mon Sep 17 00:00:00 2001 From: prescottprue Date: Sat, 16 Dec 2017 18:30:39 -0800 Subject: [PATCH 6/8] recompose is not longer a dependency (now a dev dependency) * reducer file split into utils and reducers file * mocha options moved to mocha.opts file * tests folder renamed to test --- .babelrc | 13 +- package-lock.json | 13 +- package.json | 15 +- src/firebaseConnect.js | 3 +- src/firestoreConnect.js | 5 +- src/helpers.js | 2 +- src/reducer.js | 468 +----------------- src/reducers.js | 389 +++++++++++++++ src/utils/index.js | 17 + src/utils/reducers.js | 138 ++++++ src/withFirebase.js | 2 +- src/withFirestore.js | 2 +- {tests => test}/.eslintrc | 0 test/mocha.opts | 5 + {tests => test}/mockData.js | 0 {tests => test}/setup.js | 0 {tests => test}/unit/actions/auth.spec.js | 0 {tests => test}/unit/actions/query.spec.js | 0 {tests => test}/unit/actions/storage.spec.js | 0 {tests => test}/unit/enhancer.spec.js | 0 {tests => test}/unit/firebaseConnect.spec.js | 0 {tests => test}/unit/firestoreConnect.spec.js | 0 {tests => test}/unit/helpers.spec.js | 0 {tests => test}/unit/library.spec.js | 0 {tests => test}/unit/reducer.spec.js | 3 +- {tests => test}/unit/utils/actions.spec.js | 0 {tests => test}/unit/utils/auth.spec.js | 0 {tests => test}/unit/utils/events.spec.js | 0 {tests => test}/unit/utils/index.spec.js | 0 {tests => test}/unit/utils/populate.spec.js | 0 {tests => test}/unit/utils/query.spec.js | 0 {tests => test}/unit/utils/storage.spec.js | 0 {tests => test}/unit/withFirebase.spec.js | 0 {tests => test}/unit/withFirestore.spec.js | 0 {tests => test}/utils.js | 0 35 files changed, 596 insertions(+), 479 deletions(-) create mode 100644 src/reducers.js create mode 100644 src/utils/reducers.js rename {tests => test}/.eslintrc (100%) create mode 100644 test/mocha.opts rename {tests => test}/mockData.js (100%) rename {tests => test}/setup.js (100%) rename {tests => test}/unit/actions/auth.spec.js (100%) rename {tests => test}/unit/actions/query.spec.js (100%) rename {tests => test}/unit/actions/storage.spec.js (100%) rename {tests => test}/unit/enhancer.spec.js (100%) rename {tests => test}/unit/firebaseConnect.spec.js (100%) rename {tests => test}/unit/firestoreConnect.spec.js (100%) rename {tests => test}/unit/helpers.spec.js (100%) rename {tests => test}/unit/library.spec.js (100%) rename {tests => test}/unit/reducer.spec.js (99%) rename {tests => test}/unit/utils/actions.spec.js (100%) rename {tests => test}/unit/utils/auth.spec.js (100%) rename {tests => test}/unit/utils/events.spec.js (100%) rename {tests => test}/unit/utils/index.spec.js (100%) rename {tests => test}/unit/utils/populate.spec.js (100%) rename {tests => test}/unit/utils/query.spec.js (100%) rename {tests => test}/unit/utils/storage.spec.js (100%) rename {tests => test}/unit/withFirebase.spec.js (100%) rename {tests => test}/unit/withFirestore.spec.js (100%) rename {tests => test}/utils.js (100%) diff --git a/.babelrc b/.babelrc index 2b1c6803c..4920355bf 100644 --- a/.babelrc +++ b/.babelrc @@ -9,18 +9,23 @@ }] ], "plugins": [ - ["lodash", { "id": ["lodash", "recompose"]}], + ["lodash", { "id": ["lodash"]}], "add-module-exports", "transform-object-rest-spread", "transform-object-assign", - "transform-class-properties" + "transform-class-properties", + "transform-export-extensions" ], "env": { "es": { - "comments": false + "comments": false, + "plugins": [ + ] }, "commonjs": { - "comments": false + "comments": false, + "plugins": [ + ] }, "test": { "plugins": [ diff --git a/package-lock.json b/package-lock.json index 882769bbb..e92cdd2c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2012,7 +2012,8 @@ "change-emitter": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", - "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" + "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=", + "dev": true }, "character-entities": { "version": "1.2.1", @@ -13637,6 +13638,7 @@ "version": "0.26.0", "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.26.0.tgz", "integrity": "sha512-KwOu6ztO0mN5vy3+zDcc45lgnaUoaQse/a5yLVqtzTK13czSWnFGmXbQVmnoMgDkI5POd1EwIKSbjU1V7xdZog==", + "dev": true, "requires": { "change-emitter": "0.1.6", "fbjs": "0.8.16", @@ -13657,9 +13659,9 @@ } }, "redux-firestore": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/redux-firestore/-/redux-firestore-0.2.0.tgz", - "integrity": "sha512-hiN6KSmf11zx+IMtgsUWKOe5f8Q7n83EvAGsdJuoDaPc8/+MWCwdiXUci3Ij/RBmd+Kyozd/87htssS38RyzIA==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/redux-firestore/-/redux-firestore-0.2.3.tgz", + "integrity": "sha512-2hdKlu+Tf8yD40q7ol/OtjLRLfGEZxMTIfs4+t2RtUyvGuXA13/rCQUFQJDySxlYNw1UaCL1bdt/Ny+prAC+fg==", "dev": true, "requires": { "lodash": "4.17.4", @@ -14695,7 +14697,8 @@ "symbol-observable": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", - "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=" + "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=", + "dev": true }, "symbol-tree": { "version": "3.2.2", diff --git a/package.json b/package.json index a19ff2f0c..d42d63f94 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "unpkg": "dist/react-redux-firebase.min.js", "scripts": { "clean": "rimraf es lib dist coverage", - "lint": "eslint src/** tests/**", + "lint": "eslint src/** test/**", "lint:fix": "npm run lint -- --fix", - "test": "mocha -R spec ./tests/setup.js ./tests/** --recursive --require babel-register", - "test:cov": "istanbul cover ./node_modules/mocha/bin/_mocha -- ./tests/** --recursive --report lcov --require babel-register", + "test": "mocha -R spec ./test/unit/**", + "test:cov": "istanbul cover ./node_modules/mocha/bin/_mocha ./test/unit/**", "codecov": "cat coverage/lcov.info | codecov", "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", @@ -58,8 +58,7 @@ "dependencies": { "hoist-non-react-statics": "^2.3.1", "lodash": "^4.17.4", - "prop-types": "^15.5.10", - "recompose": "^0.26.0" + "prop-types": "^15.5.10" }, "peerDependencies": { "react": "^0.14.6 || ^15.0.0-0 || ^16.0.0-0" @@ -73,6 +72,7 @@ "babel-plugin-lodash": "^3.2.11", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-class-properties": "^6.23.0", + "babel-plugin-transform-export-extensions": "^6.22.0", "babel-plugin-transform-object-assign": "^6.22.0", "babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0", @@ -101,14 +101,15 @@ "firebase": "^4.1.3", "firebase-server": "^0.10.1", "gitbook-cli": "^2.3.0", - "istanbul": "^1.1.0-alpha.1", + "istanbul": "1.1.0-alpha.1", "jsdom": "^9.12.0", "mocha": "^3.2.0", "react": "^15.4.2", "react-addons-test-utils": "^15.4.2", "react-dom": "^15.4.2", + "recompose": "^0.26.0", "redux": "3.6.0", - "redux-firestore": "^0.2.0", + "redux-firestore": "latest", "rimraf": "^2.6.2", "sinon": "^2.1.0", "sinon-chai": "^2.9.0", diff --git a/src/firebaseConnect.js b/src/firebaseConnect.js index bfb180cbf..6512048f2 100644 --- a/src/firebaseConnect.js +++ b/src/firebaseConnect.js @@ -2,9 +2,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { isEqual } from 'lodash' import hoistStatics from 'hoist-non-react-statics' -import { getDisplayName } from 'recompose' import { watchEvents, unWatchEvents } from './actions/query' -import { getEventsFromInput, createCallable } from './utils' +import { getEventsFromInput, createCallable, getDisplayName } from './utils' /** * @name createFirebaseConnect diff --git a/src/firestoreConnect.js b/src/firestoreConnect.js index 59245649b..83158a782 100644 --- a/src/firestoreConnect.js +++ b/src/firestoreConnect.js @@ -1,9 +1,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import hoistStatics from 'hoist-non-react-statics' -import { wrapDisplayName } from 'recompose' -import { createCallable } from './utils' import { isEqual } from 'lodash' +import hoistStatics from 'hoist-non-react-statics' +import { createCallable, wrapDisplayName } from './utils' /** * @name createFirestoreConnect diff --git a/src/helpers.js b/src/helpers.js index 3186c2dd0..9b99af309 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -17,7 +17,7 @@ import { } from 'lodash' import { topLevelPaths } from './constants' import { getPopulateObjs } from './utils/populate' -import { getDotStrPath } from './reducer' +import { getDotStrPath } from './utils/reducers' /** * @description Get a value from firebase using slash notation. This enables an easy diff --git a/src/reducer.js b/src/reducer.js index 2e1cd9a7e..45db48e37 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -1,457 +1,17 @@ -import { actionTypes } from './constants' -import { pick, omit, get, isArray, isObject, replace, size } from 'lodash' -import { setWith, assign, unset } from 'lodash/fp' - -const { - START, - SET, - SET_PROFILE, - MERGE, - LOGIN, - LOGOUT, - LOGIN_ERROR, - CLEAR_ERRORS, - REMOVE, - NO_VALUE, - SET_LISTENER, - UNSET_LISTENER, - AUTHENTICATION_INIT_STARTED, - AUTHENTICATION_INIT_FINISHED, - AUTH_EMPTY_CHANGE, - AUTH_LINK_SUCCESS, - UNAUTHORIZED_ERROR, - AUTH_UPDATE_SUCCESS -} = actionTypes - -/** - * Create a path array from path string - * @param {String} path - Path seperated with slashes - * @return {Array} Path as Array - * @private - */ -const pathToArr = path => path ? path.split(/\//).filter(p => !!p) : [] - -/** - * Trim leading slash from path for use with state - * @param {String} path - Path seperated with slashes - * @return {String} Path seperated with slashes - * @private - */ -const getSlashStrPath = path => pathToArr(path).join('/') - -/** - * Convert path with slashes to dot seperated path (for use with lodash get/set) - * @param {String} path - Path seperated with slashes - * @return {String} Path seperated with dots - * @private - */ -export const getDotStrPath = path => pathToArr(path).join('.') - -/** - * Recursively unset a property starting at the deep path, and unsetting the parent - * property if there are no other enumerable properties at that level. - * @param {String} path - Deep dot path of the property to unset - * @param {Boolean} [isRecursiveCall=false] - Used internally to ensure that - * the object size check is only performed after one iteration. - * @return {Object} The object with the property deeply unset - * @private - */ -export const recursiveUnset = (path, obj, isRecursiveCall = false) => { - if (!path) { - return obj - } - - if (size(get(obj, path)) > 0 && isRecursiveCall) { - return obj - } - // The object does not have any other properties at this level. Remove the - // property. - const objectWithRemovedKey = unset(path, obj) - const newPath = path.match(/\./) ? replace(path, /\.[^.]*$/, '') : '' - return recursiveUnset(newPath, objectWithRemovedKey, true) -} - -/** - * Combine reducers utility (abreveated version of redux's combineReducer). - * Turns an object whose values are different reducer functions, into a single - * reducer function. - * @param {Object} reducers An object whose values correspond to different - * reducer functions that need to be combined into one. - * @returns {Function} A reducer function that invokes every reducer inside the - * passed object, and builds a state object with the same shape. - * @private - */ -const combineReducers = (reducers) => - (state = {}, action) => - Object.keys(reducers).reduce( - (nextState, key) => { - nextState[key] = reducers[key]( - state[key], - action - ) - return nextState - }, - {} - ) - -/** - * Reducer for isInitializing state. Changed by `AUTHENTICATION_INIT_STARTED` - * and `AUTHENTICATION_INIT_FINISHED` actions. - * @param {Object} [state=false] - Current isInitializing redux state - * @param {object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @return {Object} Profile state after reduction - */ -export const isInitializingReducer = (state = false, action) => { - switch (action.type) { - case AUTHENTICATION_INIT_STARTED: - return true - case AUTHENTICATION_INIT_FINISHED: - return false - default: - return state - } -} - -/** - * Reducer for requesting state.Changed by `START`, `NO_VALUE`, and `SET` actions. - * @param {Object} [state={}] - Current requesting redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @param {String} action.path - Path of action that was dispatched - * @return {Object} Profile state after reduction - */ -export const requestingReducer = (state = {}, { type, path }) => { - switch (type) { - case START: - return { - ...state, - [getSlashStrPath(path)]: true - } - case NO_VALUE: - case SET: - return { - ...state, - [getSlashStrPath(path)]: false - } - default: - return state - } -} - -/** - * Reducer for requested state. Changed by `START`, `NO_VALUE`, and `SET` actions. - * @param {Object} [state={}] - Current requested redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @param {String} action.path - Path of action that was dispatched - * @return {Object} Profile state after reduction - */ -export const requestedReducer = (state = {}, { type, path }) => { - switch (type) { - case START: - return { - ...state, - [getSlashStrPath(path)]: false - } - case NO_VALUE: - case SET: - return { - ...state, - [getSlashStrPath(path)]: true - } - default: - return state - } -} - -/** - * Reducer for timestamps state. Changed by `START`, `NO_VALUE`, and `SET` actions. - * @param {Object} [state={}] - Current timestamps redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @param {String} action.path - Path of action that was dispatched - * @return {Object} Profile state after reduction - */ -export const timestampsReducer = (state = {}, { type, path }) => { - switch (type) { - case START: - case NO_VALUE: - case SET: - return { - ...state, - [getSlashStrPath(path)]: Date.now() - } - default: - return state - } -} - -/** - * Creates reducer for data state. Used to create data and ordered reducers. - * Changed by `SET` or `SET_ORDERED` (if actionKey === 'ordered'), `MERGE`, - * `NO_VALUE`, and `LOGOUT` actions. - * @param {Object} [state={}] - Current data redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @param {String} action.path - Path of action that was dispatched - * @return {Object} Data state after reduction - * @private - */ -const createDataReducer = (actionKey = 'data') => (state = {}, action) => { - switch (action.type) { - case SET: - return setWith(Object, getDotStrPath(action.path), action[actionKey], state) - case MERGE: - const previousData = get(state, getDotStrPath(action.path), {}) - const mergedData = assign(previousData, action[actionKey]) - return setWith(Object, getDotStrPath(action.path), mergedData, state) - case NO_VALUE: - return setWith(Object, getDotStrPath(action.path), null, state) - case REMOVE: - if (actionKey === 'data') { - return recursiveUnset(getDotStrPath(action.path), state) - } - return state - case LOGOUT: - // support keeping data when logging out - #125 - if (action.preserve) { - if (isArray(action.preserve)) { - return pick(state, action.preserve) // pick returns a new object - } else if (isObject(action.preserve)) { - return action.preserve[actionKey] - ? pick(state, action.preserve[actionKey]) - : {} - } - throw new Error('Invalid preserve parameter. It must be an Object or an Array') - } - return {} - default: - return state - } -} - -/** - * Reducer for auth state. Changed by `LOGIN`, `LOGOUT`, and `LOGIN_ERROR` actions. - * @param {Object} [state={isLoaded: false}] - Current auth redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @return {Object} Profile state after reduction - */ -export const authReducer = (state = { isLoaded: false, isEmpty: true }, action) => { - switch (action.type) { - case LOGIN: - case AUTH_UPDATE_SUCCESS: - if (!action.auth) { - return { - isEmpty: true, - isLoaded: true - } - } - const auth = action.auth.toJSON ? action.auth.toJSON() : action.auth - // Support keeping data - if (action.preserve && action.preserve.auth) { - return pick({ ...state, ...auth }, action.preserve.auth) // pick returns a new object - } - return { ...auth, isEmpty: false, isLoaded: true } - case AUTH_LINK_SUCCESS: - if (!action.payload) { - return { - isEmpty: true, - isLoaded: true - } - } - return { - ...(action.payload.toJSON ? action.payload.toJSON() : action.payload), - isEmpty: false, - isLoaded: true - } - case LOGIN_ERROR: - case AUTH_EMPTY_CHANGE: - case LOGOUT: - // Support keeping data when logging out - if (action.preserve && action.preserve.auth) { - return pick(state, action.preserve.auth) // pick returns a new object - } - return { isLoaded: true, isEmpty: true } - default: - return state - } -} - -/** - * Reducer for authError state. Changed by `LOGIN`, `LOGOUT`, `LOGIN_ERROR`, and - * `UNAUTHORIZED_ERROR` actions. - * @param {Object} [state={}] - Current authError redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @return {Object} authError state after reduction - */ -export const authErrorReducer = (state = null, action) => { - switch (action.type) { - case LOGIN: - case LOGOUT: - return null - case LOGIN_ERROR: - case UNAUTHORIZED_ERROR: - return action.authError - default: - return state - } -} - -/** - * Reducer for profile state. Changed by `SET_PROFILE`, `LOGOUT`, and - * `LOGIN_ERROR` actions. - * @param {Object} [state={isLoaded: false}] - Current profile redux state - * @param {object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @return {Object} Profile state after reduction - */ -export const profileReducer = (state = { isLoaded: false, isEmpty: true }, action) => { - switch (action.type) { - case SET_PROFILE: - if (!action.profile) { - return { - ...state, - isEmpty: true, - isLoaded: true - } - } - return { - ...action.profile, - isEmpty: false, - isLoaded: true - } - case LOGIN: - // Support keeping data when logging out - if (action.preserve && action.preserve.profile) { - return pick(state, action.preserve.profile) // pick returns a new object - } - return { - isEmpty: true, - isLoaded: false - } - case LOGOUT: - case AUTH_EMPTY_CHANGE: - // Support keeping data when logging out - if (action.preserve && action.preserve.profile) { - return pick(state, action.preserve.profile) // pick returns a new object - } - return { isLoaded: true, isEmpty: true } - default: - return state - } -} - -/** - * Reducer for errors state. Changed by `UNAUTHORIZED_ERROR`, `CLEAR_ERRORS`, - * and `LOGOUT` actions. - * @param {Object} [state=[]] - Current errors redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @param {Function} action.preserve - `not required` Filter function for - * preserving errors - * @return {Object} Profile state after reduction - */ -export const errorsReducer = (state = [], action) => { - switch (action.type) { - case LOGIN_ERROR: - case UNAUTHORIZED_ERROR: - if (!isArray(state)) { - throw new Error('Errors state must be an array') - } - return [...state, action.authError] - case LOGOUT: - case CLEAR_ERRORS: - // Support keeping errors through a filter function - if (action.preserve && action.preserve.errors) { - if (typeof action.preserve.errors !== 'function') { - throw new Error('Preserve for the errors state currently only supports functions') - } - return state.filter(action.preserve.errors) // run filter function on state - } - return [] - default: - return state - } -} - -/** - * Reducer for listeners ids. Changed by `SET_LISTENER` and `UNSET_LISTENER` - * actions. - * @param {Object} [state={}] - Current listenersById redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @return {Object} listenersById state after reduction (used in listeners) - * @private - */ -const listenersById = (state = {}, { type, path, payload }) => { - switch (type) { - case SET_LISTENER: - return { - ...state, - [payload.id]: { - id: payload.id, - path - } - } - case UNSET_LISTENER: return omit(state, [payload.id]) - default: return state - } -} - -/** - * Reducer for listeners state. Changed by `UNAUTHORIZED_ERROR` - * and `LOGOUT` actions. - * @param {Object} [state=[]] - Current allListeners redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @return {Object} allListeners state after reduction (used in listeners) - * @private - */ -const allListeners = (state = [], { type, path, payload }) => { - switch (type) { - case SET_LISTENER: return [...state, payload.id] - case UNSET_LISTENER: return state.filter(lId => lId !== payload.id) - default: return state - } -} - -/** - * Reducer for listeners state. Changed by `UNAUTHORIZED_ERROR` - * and `LOGOUT` actions. - * @param {Object} [state=[]] - Current listeners redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @return {Object} Profile state after reduction - */ -export const listenersReducer = combineReducers({ - byId: listenersById, - allIds: allListeners -}) - -/** - * Reducer for data state. Changed by `SET`, `SET_ORDERED`,`NO_VALUE`, and - * `LOGOUT` actions. - * @param {Object} [state={}] - Current data redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @param {String} action.path - Path of action that was dispatched - * @return {Object} Data state after reduction - */ -export const dataReducer = createDataReducer() - -/** - * Reducer for ordered state. Changed by `SET`, `SET_ORDERED`,`NO_VALUE`, and - * `LOGOUT` actions. - * @param {Object} [state={}] - Current data redux state - * @param {Object} action - Object containing the action that was dispatched - * @param {String} action.type - Type of action that was dispatched - * @param {String} action.path - Path of action that was dispatched - * @return {Object} Data state after reduction - */ -export const orderedReducer = createDataReducer('ordered') +import { combineReducers } from './utils/reducers' +import { + requestingReducer, + requestedReducer, + timestampsReducer, + dataReducer, + orderedReducer, + authReducer, + authErrorReducer, + profileReducer, + listenersReducer, + isInitializingReducer, + errorsReducer +} from './reducers' /** * @name firebaseReducer diff --git a/src/reducers.js b/src/reducers.js new file mode 100644 index 000000000..5db3a4859 --- /dev/null +++ b/src/reducers.js @@ -0,0 +1,389 @@ +import { pick, omit, get, isArray, isObject } from 'lodash' +import { setWith, assign } from 'lodash/fp' +import { actionTypes } from './constants' +import { + getSlashStrPath, + getDotStrPath, + recursiveUnset, + combineReducers +} from './utils/reducers' + +const { + START, + SET, + SET_PROFILE, + MERGE, + LOGIN, + LOGOUT, + LOGIN_ERROR, + CLEAR_ERRORS, + REMOVE, + NO_VALUE, + SET_LISTENER, + UNSET_LISTENER, + AUTHENTICATION_INIT_STARTED, + AUTHENTICATION_INIT_FINISHED, + AUTH_EMPTY_CHANGE, + AUTH_LINK_SUCCESS, + UNAUTHORIZED_ERROR, + AUTH_UPDATE_SUCCESS +} = actionTypes + +/** + * Reducer for isInitializing state. Changed by `AUTHENTICATION_INIT_STARTED` + * and `AUTHENTICATION_INIT_FINISHED` actions. + * @param {Object} [state=false] - Current isInitializing redux state + * @param {object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @return {Object} Profile state after reduction + */ +export const isInitializingReducer = (state = false, action) => { + switch (action.type) { + case AUTHENTICATION_INIT_STARTED: + return true + case AUTHENTICATION_INIT_FINISHED: + return false + default: + return state + } +} + +/** + * Reducer for requesting state.Changed by `START`, `NO_VALUE`, and `SET` actions. + * @param {Object} [state={}] - Current requesting redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @param {String} action.path - Path of action that was dispatched + * @return {Object} Profile state after reduction + */ +export const requestingReducer = (state = {}, { type, path }) => { + switch (type) { + case START: + return { + ...state, + [getSlashStrPath(path)]: true + } + case NO_VALUE: + case SET: + return { + ...state, + [getSlashStrPath(path)]: false + } + default: + return state + } +} + +/** + * Reducer for requested state. Changed by `START`, `NO_VALUE`, and `SET` actions. + * @param {Object} [state={}] - Current requested redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @param {String} action.path - Path of action that was dispatched + * @return {Object} Profile state after reduction + */ +export const requestedReducer = (state = {}, { type, path }) => { + switch (type) { + case START: + return { + ...state, + [getSlashStrPath(path)]: false + } + case NO_VALUE: + case SET: + return { + ...state, + [getSlashStrPath(path)]: true + } + default: + return state + } +} + +/** + * Reducer for timestamps state. Changed by `START`, `NO_VALUE`, and `SET` actions. + * @param {Object} [state={}] - Current timestamps redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @param {String} action.path - Path of action that was dispatched + * @return {Object} Profile state after reduction + */ +export const timestampsReducer = (state = {}, { type, path }) => { + switch (type) { + case START: + case NO_VALUE: + case SET: + return { + ...state, + [getSlashStrPath(path)]: Date.now() + } + default: + return state + } +} + +/** + * Creates reducer for data state. Used to create data and ordered reducers. + * Changed by `SET` or `SET_ORDERED` (if actionKey === 'ordered'), `MERGE`, + * `NO_VALUE`, and `LOGOUT` actions. + * @param {Object} [state={}] - Current data redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @param {String} action.path - Path of action that was dispatched + * @return {Object} Data state after reduction + * @private + */ +const createDataReducer = (actionKey = 'data') => (state = {}, action) => { + switch (action.type) { + case SET: + return setWith(Object, getDotStrPath(action.path), action[actionKey], state) + case MERGE: + const previousData = get(state, getDotStrPath(action.path), {}) + const mergedData = assign(previousData, action[actionKey]) + return setWith(Object, getDotStrPath(action.path), mergedData, state) + case NO_VALUE: + return setWith(Object, getDotStrPath(action.path), null, state) + case REMOVE: + if (actionKey === 'data') { + return recursiveUnset(getDotStrPath(action.path), state) + } + return state + case LOGOUT: + // support keeping data when logging out - #125 + if (action.preserve) { + if (isArray(action.preserve)) { + return pick(state, action.preserve) // pick returns a new object + } else if (isObject(action.preserve)) { + return action.preserve[actionKey] + ? pick(state, action.preserve[actionKey]) + : {} + } + throw new Error('Invalid preserve parameter. It must be an Object or an Array') + } + return {} + default: + return state + } +} + +/** + * Reducer for auth state. Changed by `LOGIN`, `LOGOUT`, and `LOGIN_ERROR` actions. + * @param {Object} [state={isLoaded: false}] - Current auth redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @return {Object} Profile state after reduction + */ +export const authReducer = (state = { isLoaded: false, isEmpty: true }, action) => { + switch (action.type) { + case LOGIN: + case AUTH_UPDATE_SUCCESS: + if (!action.auth) { + return { + isEmpty: true, + isLoaded: true + } + } + const auth = action.auth.toJSON ? action.auth.toJSON() : action.auth + // Support keeping data + if (action.preserve && action.preserve.auth) { + return pick({ ...state, ...auth }, action.preserve.auth) // pick returns a new object + } + return { ...auth, isEmpty: false, isLoaded: true } + case AUTH_LINK_SUCCESS: + if (!action.payload) { + return { + isEmpty: true, + isLoaded: true + } + } + return { + ...(action.payload.toJSON ? action.payload.toJSON() : action.payload), + isEmpty: false, + isLoaded: true + } + case LOGIN_ERROR: + case AUTH_EMPTY_CHANGE: + case LOGOUT: + // Support keeping data when logging out + if (action.preserve && action.preserve.auth) { + return pick(state, action.preserve.auth) // pick returns a new object + } + return { isLoaded: true, isEmpty: true } + default: + return state + } +} + +/** + * Reducer for authError state. Changed by `LOGIN`, `LOGOUT`, `LOGIN_ERROR`, and + * `UNAUTHORIZED_ERROR` actions. + * @param {Object} [state={}] - Current authError redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @return {Object} authError state after reduction + */ +export const authErrorReducer = (state = null, action) => { + switch (action.type) { + case LOGIN: + case LOGOUT: + return null + case LOGIN_ERROR: + case UNAUTHORIZED_ERROR: + return action.authError + default: + return state + } +} + +/** + * Reducer for profile state. Changed by `SET_PROFILE`, `LOGOUT`, and + * `LOGIN_ERROR` actions. + * @param {Object} [state={isLoaded: false}] - Current profile redux state + * @param {object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @return {Object} Profile state after reduction + */ +export const profileReducer = (state = { isLoaded: false, isEmpty: true }, action) => { + switch (action.type) { + case SET_PROFILE: + if (!action.profile) { + return { + ...state, + isEmpty: true, + isLoaded: true + } + } + return { + ...action.profile, + isEmpty: false, + isLoaded: true + } + case LOGIN: + // Support keeping data when logging out + if (action.preserve && action.preserve.profile) { + return pick(state, action.preserve.profile) // pick returns a new object + } + return { + isEmpty: true, + isLoaded: false + } + case LOGOUT: + case AUTH_EMPTY_CHANGE: + // Support keeping data when logging out + if (action.preserve && action.preserve.profile) { + return pick(state, action.preserve.profile) // pick returns a new object + } + return { isLoaded: true, isEmpty: true } + default: + return state + } +} + +/** + * Reducer for errors state. Changed by `UNAUTHORIZED_ERROR`, `CLEAR_ERRORS`, + * and `LOGOUT` actions. + * @param {Object} [state=[]] - Current errors redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @param {Function} action.preserve - `not required` Filter function for + * preserving errors + * @return {Object} Profile state after reduction + */ +export const errorsReducer = (state = [], action) => { + switch (action.type) { + case LOGIN_ERROR: + case UNAUTHORIZED_ERROR: + if (!isArray(state)) { + throw new Error('Errors state must be an array') + } + return [...state, action.authError] + case LOGOUT: + case CLEAR_ERRORS: + // Support keeping errors through a filter function + if (action.preserve && action.preserve.errors) { + if (typeof action.preserve.errors !== 'function') { + throw new Error('Preserve for the errors state currently only supports functions') + } + return state.filter(action.preserve.errors) // run filter function on state + } + return [] + default: + return state + } +} + +/** + * Reducer for listeners ids. Changed by `SET_LISTENER` and `UNSET_LISTENER` + * actions. + * @param {Object} [state={}] - Current listenersById redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @return {Object} listenersById state after reduction (used in listeners) + * @private + */ +const listenersById = (state = {}, { type, path, payload }) => { + switch (type) { + case SET_LISTENER: + return { + ...state, + [payload.id]: { + id: payload.id, + path + } + } + case UNSET_LISTENER: return omit(state, [payload.id]) + default: return state + } +} + +/** + * Reducer for listeners state. Changed by `UNAUTHORIZED_ERROR` + * and `LOGOUT` actions. + * @param {Object} [state=[]] - Current allListeners redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @return {Object} allListeners state after reduction (used in listeners) + * @private + */ +const allListeners = (state = [], { type, path, payload }) => { + switch (type) { + case SET_LISTENER: return [...state, payload.id] + case UNSET_LISTENER: return state.filter(lId => lId !== payload.id) + default: return state + } +} + +/** + * Reducer for listeners state. Changed by `UNAUTHORIZED_ERROR` + * and `LOGOUT` actions. + * @param {Object} [state=[]] - Current listeners redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @return {Object} Profile state after reduction + */ +export const listenersReducer = combineReducers({ + byId: listenersById, + allIds: allListeners +}) + +/** + * Reducer for data state. Changed by `SET`, `SET_ORDERED`,`NO_VALUE`, and + * `LOGOUT` actions. + * @param {Object} [state={}] - Current data redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @param {String} action.path - Path of action that was dispatched + * @return {Object} Data state after reduction + */ +export const dataReducer = createDataReducer() + +/** + * Reducer for ordered state. Changed by `SET`, `SET_ORDERED`,`NO_VALUE`, and + * `LOGOUT` actions. + * @param {Object} [state={}] - Current data redux state + * @param {Object} action - Object containing the action that was dispatched + * @param {String} action.type - Type of action that was dispatched + * @param {String} action.path - Path of action that was dispatched + * @return {Object} Data state after reduction + */ +export const orderedReducer = createDataReducer('ordered') diff --git a/src/utils/index.js b/src/utils/index.js index f84f78d76..25f72c8eb 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -7,3 +7,20 @@ export { getEventsFromInput } from './events' * @param {Function|Object|Array|String} Callable function or value of return for new function */ export const createCallable = f => isFunction(f) ? f : () => f + +export const getDisplayName = Component => { + if (typeof Component === 'string') { + return Component + } + + if (!Component) { + return undefined + } + + return Component.displayName || Component.name || 'Component' +} + +export default getDisplayName + +export const wrapDisplayName = (BaseComponent, hocName) => + `${hocName}(${getDisplayName(BaseComponent)})` diff --git a/src/utils/reducers.js b/src/utils/reducers.js new file mode 100644 index 000000000..4a6705d08 --- /dev/null +++ b/src/utils/reducers.js @@ -0,0 +1,138 @@ +import { get, replace, size } from 'lodash' +import { unset } from 'lodash/fp' + +/** + * Create a path array from path string + * @param {String} path - Path seperated with slashes + * @return {Array} Path as Array + * @private + */ +export function pathToArr (path) { + return path ? path.split(/\//).filter(p => !!p) : [] +} + +/** + * Trim leading slash from path for use with state + * @param {String} path - Path seperated with slashes + * @return {String} Path seperated with slashes + * @private + */ +export function getSlashStrPath (path) { + return pathToArr(path).join('/') +} + +/** + * Convert path with slashes to dot seperated path (for use with lodash get/set) + * @param {String} path - Path seperated with slashes + * @return {String} Path seperated with dots + * @private + */ +export function getDotStrPath (path) { + return pathToArr(path).join('.') +} + +/** + * Combine reducers utility (abreveated version of redux's combineReducer). + * Turns an object whose values are different reducer functions, into a single + * reducer function. + * @param {Object} reducers An object whose values correspond to different + * reducer functions that need to be combined into one. + * @returns {Function} A reducer function that invokes every reducer inside the + * passed object, and builds a state object with the same shape. + * @private + */ +export const combineReducers = reducers => + (state = {}, action) => + Object.keys(reducers).reduce( + (nextState, key) => { + nextState[key] = reducers[key]( // eslint-disable-line no-param-reassign + state[key], + action + ) + return nextState + }, + {} + ) + +/** + * Get path from meta data. Path is used with lodash's setWith to set deep + * data within reducers. + * @param {Object} meta - Action meta data object + * @param {String} meta.collection - Name of Collection for which the action + * is to be handled. + * @param {String} meta.doc - Name of Document for which the action is to be + * handled. + * @param {Array} meta.subcollections - Subcollections of data + * @param {String} meta.storeAs - Another key within redux store that the + * action associates with (used for storing data under a path different + * from its collection/document) + * @return {String} String path to be used within reducer + */ +export function pathFromMeta (meta) { + if (!meta) { + throw new Error('Action meta is required to build path for reducers.') + } + const { collection, doc, subcollections, storeAs } = meta + if (storeAs) { + return storeAs + } + if (!collection) { + throw new Error('Collection is required to construct reducer path.') + } + let basePath = collection + if (doc) { + basePath += `.${doc}` + } + if (!subcollections) { + return basePath + } + const mappedCollections = subcollections.map(pathFromMeta) + return basePath.concat(`.${mappedCollections.join('.')}`) +} + +/** + * Recursively unset a property starting at the deep path, and unsetting the parent + * property if there are no other enumerable properties at that level. + * @param {String} path - Deep dot path of the property to unset + * @param {Boolean} [isRecursiveCall=false] - Used internally to ensure that + * the object size check is only performed after one iteration. + * @return {Object} The object with the property deeply unset + * @private + */ +export const recursiveUnset = (path, obj, isRecursiveCall = false) => { + if (!path) { + return obj + } + + if (size(get(obj, path)) > 0 && isRecursiveCall) { + return obj + } + // The object does not have any other properties at this level. Remove the + // property. + const objectWithRemovedKey = unset(path, obj) + const newPath = path.match(/\./) ? replace(path, /\.[^.]*$/, '') : '' + return recursiveUnset(newPath, objectWithRemovedKey, true) +} + +/** + * Update a single item within an array + * @param {Array} array - Array within which to update item + * @param {String} itemId - Id of item to update + * @param {Function} updateItemCallback - Callback dictacting how the item + * is updated + * @return {Array} Array with item updated + */ +export function updateItemInArray (array, itemId, updateItemCallback) { + const updatedItems = array.map((item) => { + if (item.id !== itemId) { + // Since we only want to update one item, preserve all others as they are now + return item + } + + // Use the provided callback to create an updated item + const updatedItem = updateItemCallback(item) + return updatedItem + }) + + return updatedItems +} diff --git a/src/withFirebase.js b/src/withFirebase.js index a43d9096f..722063eac 100644 --- a/src/withFirebase.js +++ b/src/withFirebase.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import hoistStatics from 'hoist-non-react-statics' -import { wrapDisplayName } from 'recompose' +import { wrapDisplayName } from './utils' /** * @name createWithFirebase diff --git a/src/withFirestore.js b/src/withFirestore.js index 5df737135..c3d4b6914 100644 --- a/src/withFirestore.js +++ b/src/withFirestore.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import hoistStatics from 'hoist-non-react-statics' -import { wrapDisplayName } from 'recompose' +import { wrapDisplayName } from './utils' /** * @name createWithFirestore diff --git a/tests/.eslintrc b/test/.eslintrc similarity index 100% rename from tests/.eslintrc rename to test/.eslintrc diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 000000000..3d28ff6f4 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,5 @@ +--require babel-core/register +--require ./test/setup +--recursive +--colors +--report lcov diff --git a/tests/mockData.js b/test/mockData.js similarity index 100% rename from tests/mockData.js rename to test/mockData.js diff --git a/tests/setup.js b/test/setup.js similarity index 100% rename from tests/setup.js rename to test/setup.js diff --git a/tests/unit/actions/auth.spec.js b/test/unit/actions/auth.spec.js similarity index 100% rename from tests/unit/actions/auth.spec.js rename to test/unit/actions/auth.spec.js diff --git a/tests/unit/actions/query.spec.js b/test/unit/actions/query.spec.js similarity index 100% rename from tests/unit/actions/query.spec.js rename to test/unit/actions/query.spec.js diff --git a/tests/unit/actions/storage.spec.js b/test/unit/actions/storage.spec.js similarity index 100% rename from tests/unit/actions/storage.spec.js rename to test/unit/actions/storage.spec.js diff --git a/tests/unit/enhancer.spec.js b/test/unit/enhancer.spec.js similarity index 100% rename from tests/unit/enhancer.spec.js rename to test/unit/enhancer.spec.js diff --git a/tests/unit/firebaseConnect.spec.js b/test/unit/firebaseConnect.spec.js similarity index 100% rename from tests/unit/firebaseConnect.spec.js rename to test/unit/firebaseConnect.spec.js diff --git a/tests/unit/firestoreConnect.spec.js b/test/unit/firestoreConnect.spec.js similarity index 100% rename from tests/unit/firestoreConnect.spec.js rename to test/unit/firestoreConnect.spec.js diff --git a/tests/unit/helpers.spec.js b/test/unit/helpers.spec.js similarity index 100% rename from tests/unit/helpers.spec.js rename to test/unit/helpers.spec.js diff --git a/tests/unit/library.spec.js b/test/unit/library.spec.js similarity index 100% rename from tests/unit/library.spec.js rename to test/unit/library.spec.js diff --git a/tests/unit/reducer.spec.js b/test/unit/reducer.spec.js similarity index 99% rename from tests/unit/reducer.spec.js rename to test/unit/reducer.spec.js index 4a6afdd96..ef23ee1ea 100644 --- a/tests/unit/reducer.spec.js +++ b/test/unit/reducer.spec.js @@ -1,6 +1,7 @@ import { setWith } from 'lodash/fp' import { actionTypes } from '../../src/constants' -import firebaseReducer, { getDotStrPath } from '../../src/reducer' +import firebaseReducer from '../../src/reducer' +import { getDotStrPath } from '../../src/utils/reducers' const initialState = { auth: { isLoaded: false, isEmpty: true }, diff --git a/tests/unit/utils/actions.spec.js b/test/unit/utils/actions.spec.js similarity index 100% rename from tests/unit/utils/actions.spec.js rename to test/unit/utils/actions.spec.js diff --git a/tests/unit/utils/auth.spec.js b/test/unit/utils/auth.spec.js similarity index 100% rename from tests/unit/utils/auth.spec.js rename to test/unit/utils/auth.spec.js diff --git a/tests/unit/utils/events.spec.js b/test/unit/utils/events.spec.js similarity index 100% rename from tests/unit/utils/events.spec.js rename to test/unit/utils/events.spec.js diff --git a/tests/unit/utils/index.spec.js b/test/unit/utils/index.spec.js similarity index 100% rename from tests/unit/utils/index.spec.js rename to test/unit/utils/index.spec.js diff --git a/tests/unit/utils/populate.spec.js b/test/unit/utils/populate.spec.js similarity index 100% rename from tests/unit/utils/populate.spec.js rename to test/unit/utils/populate.spec.js diff --git a/tests/unit/utils/query.spec.js b/test/unit/utils/query.spec.js similarity index 100% rename from tests/unit/utils/query.spec.js rename to test/unit/utils/query.spec.js diff --git a/tests/unit/utils/storage.spec.js b/test/unit/utils/storage.spec.js similarity index 100% rename from tests/unit/utils/storage.spec.js rename to test/unit/utils/storage.spec.js diff --git a/tests/unit/withFirebase.spec.js b/test/unit/withFirebase.spec.js similarity index 100% rename from tests/unit/withFirebase.spec.js rename to test/unit/withFirebase.spec.js diff --git a/tests/unit/withFirestore.spec.js b/test/unit/withFirestore.spec.js similarity index 100% rename from tests/unit/withFirestore.spec.js rename to test/unit/withFirestore.spec.js diff --git a/tests/utils.js b/test/utils.js similarity index 100% rename from tests/utils.js rename to test/utils.js From c5d36ded9a1dbe46570d2736063ba93fbefb4bf9 Mon Sep 17 00:00:00 2001 From: prescottprue Date: Sat, 16 Dec 2017 22:16:02 -0800 Subject: [PATCH 7/8] Moved babel require out of mocha.opts back into npm script (does not work on travis) --- package.json | 4 ++-- test/mocha.opts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d42d63f94..46b5697b3 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "clean": "rimraf es lib dist coverage", "lint": "eslint src/** test/**", "lint:fix": "npm run lint -- --fix", - "test": "mocha -R spec ./test/unit/**", - "test:cov": "istanbul cover ./node_modules/mocha/bin/_mocha ./test/unit/**", + "test": "mocha -R spec ./test/unit/** -- --require babel-core/register", + "test:cov": "istanbul cover ./node_modules/mocha/bin/_mocha ./test/unit/** -- --require babel-core/register", "codecov": "cat coverage/lcov.info | codecov", "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", diff --git a/test/mocha.opts b/test/mocha.opts index 3d28ff6f4..0c8004348 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,3 @@ ---require babel-core/register --require ./test/setup --recursive --colors From a3c7b1199672b800b1865acaf8eb2bb8857f1c5d Mon Sep 17 00:00:00 2001 From: prescottprue Date: Sat, 16 Dec 2017 22:24:49 -0800 Subject: [PATCH 8/8] ignore mocha opts from eslint. --- .eslintignore | 1 + package.json | 4 ++-- test/mocha.opts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.eslintignore b/.eslintignore index 413d176a7..67988b0d0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ coverage/** node_modules/** _book/** _site/** +test/mocha.opts diff --git a/package.json b/package.json index 46b5697b3..d42d63f94 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "clean": "rimraf es lib dist coverage", "lint": "eslint src/** test/**", "lint:fix": "npm run lint -- --fix", - "test": "mocha -R spec ./test/unit/** -- --require babel-core/register", - "test:cov": "istanbul cover ./node_modules/mocha/bin/_mocha ./test/unit/** -- --require babel-core/register", + "test": "mocha -R spec ./test/unit/**", + "test:cov": "istanbul cover ./node_modules/mocha/bin/_mocha ./test/unit/**", "codecov": "cat coverage/lcov.info | codecov", "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", diff --git a/test/mocha.opts b/test/mocha.opts index 0c8004348..3d28ff6f4 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,4 @@ +--require babel-core/register --require ./test/setup --recursive --colors