├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── package.json ├── src ├── components │ ├── FalcorProvider.js │ ├── createStore.js │ ├── duck.js │ └── reduxFalcor.js └── index.js ├── static └── logo.png ├── test ├── .gitkeep ├── integration │ ├── appContainer.js │ ├── main.js │ └── store.js ├── root.js ├── src │ ├── app.js │ └── model.js ├── test_create_store.js ├── test_duck.js ├── test_falcor_provider.js ├── test_integration.js └── test_redux_falcor.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "mocha": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "quotes": [2, "single"], 13 | "strict": [2, "never"], 14 | "babel/generator-star-spacing": 1, 15 | "babel/new-cap": 1, 16 | "babel/object-shorthand": 1, 17 | "babel/arrow-parens": 1, 18 | "babel/no-await-in-loop": 1, 19 | "react/jsx-uses-react": 2, 20 | "react/jsx-uses-vars": 2, 21 | "react/react-in-jsx-scope": 2 22 | }, 23 | "plugins": [ 24 | "babel", 25 | "react" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | dist 5 | lib 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | /.*/ 3 | /test/ 4 | /.eslint* 5 | /.gitignore 6 | /.npmignore 7 | /.travis.yml 8 | /webpack.* 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0.0" 4 | script: 5 | - npm run lint 6 | - npm test 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | Every release, along with the migration instructions, is documented on the Github [Releases](https://github.com/ekosz/redux-falcor/releases) page. 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Visit https://supportedsource.org/projects/redux-falcor to get a license. 2 | 3 | Depending on your company's size, the license may be free. It is free for individuals. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Redux Falcor](static/logo.png "Redux Falcor") 2 | 3 | Redux Falcor connects Redux applications to the Falcor API. 4 | 5 | [![build status](https://img.shields.io/travis/ekosz/redux-falcor/master.svg?style=flat-square)](https://travis-ci.org/ekosz/redux-falcor) 6 | [![npm version](https://img.shields.io/npm/v/redux-falcor.svg?style=flat-square)](https://www.npmjs.com/package/redux-falcor) 7 | 8 | [Change Log](https://github.com/ekosz/redux-falcor/releases) 9 | 10 | ### Installation 11 | 12 | To install: 13 | 14 | ``` 15 | npm install --save redux-falcor 16 | ``` 17 | 18 | ### Usage 19 | 20 | First include `redux-falcor` in the initial setup of your application. 21 | 22 | ```js 23 | import { createStore, combineReducers } from 'redux'; 24 | import { reducer as falcorReducer } from 'redux-falcor'; 25 | 26 | const reducers = combineReducers({ 27 | falcor: falcorReducer, 28 | // Other reducers here 29 | }); 30 | 31 | 32 | const store = finalCreateStore(reducers); 33 | ``` 34 | 35 | Next attach the `FalcorProvider` at the top level of your react application. 36 | 37 | ```js 38 | import { Provider } from 'react-redux'; 39 | import { FalcorProvider } from 'redux-falcor'; 40 | import { Model } from 'falcor'; 41 | import store from './store'; // Your redux store 42 | 43 | // The falcor model that redux-falcor will query 44 | const falcor = new Model({ 45 | cache: { 46 | // Optional data here 47 | } 48 | }); 49 | 50 | const application = ( 51 | 52 | 53 | {/* The rest here */} 54 | 55 | 56 | ); 57 | 58 | React.render(application, document.getElementById('app')); 59 | ``` 60 | 61 | With that in place we can now connect our components to the falcor-store 62 | provided by `redux-falcor`. This should feel familiar to `react-redux`. 63 | 64 | ```js 65 | import React, { Component } from 'react'; 66 | import { connect } from 'react-redux'; 67 | import { reduxFalcor } from 'redux-falcor'; 68 | import App from './App'; 69 | 70 | class AppContainer extends Component { 71 | fetchFalcorDeps() { 72 | return this.props.falcor.get( 73 | ['currentUser', App.queries.user()], 74 | ); 75 | } 76 | 77 | handleClick(event) { 78 | event.preventDefault(); 79 | 80 | this.props.falcor.call(['some', 'path']).then(() => { 81 | console.log('Some path called'); 82 | }).catch(() => { 83 | console.error('Some path failed'); 84 | }); 85 | } 86 | 87 | render() { 88 | return ( 89 | 93 | ); 94 | } 95 | } 96 | 97 | function mapStateToProps(state) { 98 | return { 99 | currentUser: state.falcor.currentUser || {} 100 | }; 101 | } 102 | 103 | export default connect( 104 | mapStateToProps, 105 | )(reduxFalcor(AppContainer)); 106 | ``` 107 | 108 | You can see `reduxFalcor` has done two things for us. First off, our Falcor 109 | model has been provided to our Component via the `falcor` prop. This is useful 110 | for creating event handlers that call out to our `falcor-router`. 111 | 112 | Secondly, if we define the method `fetchFalcorDeps`, `redux-falcor` will 113 | automatically call that function when the component is first mounted to the DOM 114 | as well as whenever the Falcor cache has been invalidated. This method should 115 | return a promise that fetches all of our Falcor dependencies for this 116 | component. 117 | 118 | **Warning** 119 | 120 | Because Falcor is intrinsically asynchronous, your code can not rely on any one 121 | piece of state being present when rendering. In the example above we give 122 | a default for `currentUser` when we haven't fetched that piece of data yet. 123 | 124 | ### Thanks 125 | 126 | This library was *heavy* influenced by @gaearon and his work on 127 | `react-redux`(https://github.com/rackt/react-redux). I would also like to thank 128 | @trxcllnt for helping solve some of the problems with earlier versions of 129 | `redux-falcor`. This library would not be as useful as it is now without his 130 | input. 131 | 132 | ### Licence 133 | 134 | Visit https://supportedsource.org/projects/redux-falcor to get a license. 135 | 136 | Depending on your company's size, the license may be free. It is free for individuals. 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-falcor", 3 | "version": "3.0.1", 4 | "description": "A helper library for integratig Redux & Falcor", 5 | "main": "lib/index.js", 6 | "jsnext:main": "src/index.js", 7 | "scripts": { 8 | "clean": "rimraf lib dist", 9 | "lint": "eslint src test", 10 | "test": "mocha --compilers js:babel-core/register --recursive", 11 | "test:watch": "npm test -- --watch", 12 | "check": "npm run lint && npm run test", 13 | "build:lib": "babel src --out-dir lib", 14 | "build:umd": "webpack src/index.js dist/falcorRedux.js --config webpack.config.js", 15 | "build": "npm run build:lib && npm run build:umd", 16 | "preversion": "npm run clean && npm run check", 17 | "version": "npm run build", 18 | "postversion": "git push && git push --tags && npm run clean", 19 | "prepublish": "npm run clean && npm run build" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/ekosz/redux-falcor.git" 24 | }, 25 | "keywords": [ 26 | "redux", 27 | "falcor" 28 | ], 29 | "author": "Eric Koslow (https://github.com/ekosz)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/ekosz/redux-falcor/issues" 33 | }, 34 | "homepage": "https://github.com/ekosz/redux-falcor#readme", 35 | "dependencies": { 36 | "deepmerge": "^0.2.10", 37 | "falcor-expand-cache": "^0.0.3", 38 | "falcor-json-graph": "^2.0.0", 39 | "hoist-non-react-statics": "^1.0.3", 40 | "invariant": "^2.2.0", 41 | "tiny-uuid": "^1.0.0" 42 | }, 43 | "peerDependencies": { 44 | "falcor": "^0.1.13", 45 | "react": "^0.14.0 || ^15.0.0", 46 | "redux": "^2.0.0 || ^3.0.0" 47 | }, 48 | "devDependencies": { 49 | "babel-cli": "^6.3.17", 50 | "babel-core": "^6.3.26", 51 | "babel-eslint": "^4.1.6", 52 | "babel-loader": "^6.2.0", 53 | "babel-preset-es2015": "^6.3.13", 54 | "babel-preset-react": "^6.3.13", 55 | "babel-preset-stage-0": "^6.3.13", 56 | "deep-equal": "^1.0.1", 57 | "enzyme": "^2.4.1", 58 | "eslint": "^1.10.3", 59 | "eslint-plugin-babel": "^3.0.0", 60 | "eslint-plugin-react": "^3.13.1", 61 | "expect": "^1.12.1", 62 | "falcor": "^0.1.13", 63 | "falcor-router": "^0.2.9", 64 | "jsdom": "^9.5.0", 65 | "mocha": "^2.3.4", 66 | "react": "^0.14.3", 67 | "react-addons-test-utils": "^0.14.3", 68 | "react-dom": "^0.14.3", 69 | "react-redux": "^4.4.5", 70 | "redux": "^3.6.0", 71 | "rimraf": "^2.4.3", 72 | "sinon": "^1.17.6", 73 | "webpack": "^1.12.2" 74 | }, 75 | "npmFileMap": [ 76 | { 77 | "basePath": "/dist/", 78 | "files": [ 79 | "*.js" 80 | ] 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /src/components/FalcorProvider.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes, Children } from 'react'; 2 | import expandCache from 'falcor-expand-cache'; 3 | 4 | import createStore from './createStore'; 5 | 6 | function debounce(func, wait) { 7 | let timeout; 8 | return function doDebounce() { 9 | const context = this; 10 | const args = arguments; 11 | const later = () => { 12 | timeout = null; 13 | func.apply(context, args); 14 | }; 15 | clearTimeout(timeout); 16 | timeout = setTimeout(later, wait); 17 | }; 18 | } 19 | 20 | function attachOnChange(falcor, store) { 21 | // TODO: Throttle requests here 22 | const handler = debounce(() => { 23 | store.trigger(expandCache(falcor.getCache())); 24 | }, 50); 25 | 26 | const root = falcor._root; 27 | if (!root.onChange) { 28 | root.onChange = handler; 29 | return; 30 | } 31 | 32 | const oldOnChange = root.onChange; 33 | root.onChange = () => { 34 | oldOnChange(); 35 | handler(); 36 | }; 37 | } 38 | 39 | export default class FalcorProvider extends Component { 40 | static propTypes = { 41 | falcor: PropTypes.object.isRequired, 42 | store: PropTypes.object.isRequired, 43 | children: PropTypes.element.isRequired, 44 | }; 45 | 46 | static childContextTypes = { 47 | falcor: PropTypes.object.isRequired, 48 | falcorStore: PropTypes.object.isRequired, 49 | }; 50 | 51 | constructor(props, context) { 52 | super(props, context); 53 | this.falcor = props.falcor; 54 | this.falcorStore = createStore(props.store); 55 | attachOnChange(props.falcor, this.falcorStore); 56 | } 57 | 58 | getChildContext() { 59 | return { 60 | falcor: this.falcor, 61 | falcorStore: this.falcorStore, 62 | }; 63 | } 64 | 65 | render() { 66 | const { children } = this.props; 67 | return Children.only(children); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/createStore.js: -------------------------------------------------------------------------------- 1 | import { update } from './duck'; 2 | 3 | export default function createStore(reduxStore) { 4 | const listeners = []; 5 | 6 | function subscribe(listener) { 7 | listeners.push(listener); 8 | let isSubscribed = true; 9 | 10 | return function unsubscribe() { 11 | if (!isSubscribed) return; 12 | 13 | isSubscribed = false; 14 | listeners.splice(listeners.indexOf(listener), 1); 15 | }; 16 | } 17 | 18 | function trigger(cache) { 19 | // Update the redux with the changes 20 | reduxStore.dispatch(update(cache)); 21 | 22 | // Trigger listeners to refetch possible invalidated data 23 | listeners.slice().forEach((listener) => listener()); 24 | return cache; 25 | } 26 | 27 | return { 28 | subscribe, 29 | trigger, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/duck.js: -------------------------------------------------------------------------------- 1 | const UPDATE = 'redux-falcor/UPDATE'; 2 | 3 | export default function reduxFalcorReducer(state = {}, action) { 4 | switch (action.type) { 5 | case UPDATE: 6 | return { ...action.payload }; 7 | default: 8 | return state; 9 | } 10 | } 11 | 12 | export function update(falcorCache) { 13 | return { 14 | type: UPDATE, 15 | payload: falcorCache, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/reduxFalcor.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import invariant from 'invariant'; 3 | import hoistStatics from 'hoist-non-react-statics'; 4 | 5 | function getDisplayName(WrappedComponent) { 6 | return WrappedComponent.displayName || WrappedComponent.name || 'Component'; 7 | } 8 | 9 | function noop() {} 10 | 11 | export default function reduxFalcor(WrappedComponent) { 12 | class ReduxFalcor extends Component { 13 | constructor(props, context) { 14 | super(props, context); 15 | 16 | this.falcor = props.falcor || context.falcor; 17 | this.falcorStore = props.falcorStore || context.falcorStore; 18 | 19 | invariant(this.falcorStore, 20 | `Could not find "falcorStore" in either the context or ` + 21 | `props of "${this.constructor.displayName}". ` + 22 | `Either wrap the root component in a , ` + 23 | `or explicitly pass "falcorStore" as a prop to "${this.constructor.displayName}".` 24 | ); 25 | } 26 | 27 | componentDidMount() { 28 | this.trySubscribe(); 29 | } 30 | 31 | componentWillUnmount() { 32 | this.tryUnsubscribe(); 33 | } 34 | 35 | trySubscribe() { 36 | if (!this.unsubscribe) { 37 | this.unsubscribe = this.falcorStore.subscribe(::this.handleChange); 38 | this.handleChange(); 39 | } 40 | } 41 | 42 | handleChange() { 43 | const { wrappedInstance } = this.refs; 44 | 45 | if (!this.unsubscribe) return; 46 | if (!(typeof wrappedInstance.fetchFalcorDeps === 'function')) return; 47 | 48 | wrappedInstance.fetchFalcorDeps().then(noop); 49 | } 50 | 51 | tryUnsubscribe() { 52 | if (this.unsubscribe) { 53 | this.unsubscribe(); 54 | this.unsubscribe = null; 55 | } 56 | } 57 | 58 | render() { 59 | return ( 60 | 65 | ); 66 | } 67 | } 68 | 69 | ReduxFalcor.displayName = `ReduxFalcor(${getDisplayName(WrappedComponent)})`; 70 | ReduxFalcor.WrappedComponent = WrappedComponent; 71 | 72 | ReduxFalcor.propTypes = { 73 | falcorStore: PropTypes.object, 74 | falcor: PropTypes.object, 75 | }; 76 | 77 | ReduxFalcor.contextTypes = { 78 | falcorStore: PropTypes.object.isRequired, 79 | falcor: PropTypes.object.isRequired, 80 | }; 81 | 82 | return hoistStatics(ReduxFalcor, WrappedComponent); 83 | } 84 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as reducer } from './components/duck'; 2 | export { default as FalcorProvider } from './components/FalcorProvider'; 3 | export { default as reduxFalcor } from './components/reduxFalcor'; 4 | -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekosz/redux-falcor/8d5b0ec450b8ddf2cf0ca6e71f60e494c9e1cb73/static/logo.png -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekosz/redux-falcor/8d5b0ec450b8ddf2cf0ca6e71f60e494c9e1cb73/test/.gitkeep -------------------------------------------------------------------------------- /test/integration/appContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxFalcor } from '../../src/index.js'; 4 | import App from '../src/app.js'; 5 | 6 | class AppContainer extends Component { 7 | // fetchFalcorDeps() { 8 | // return Promise.resolve(); 9 | // } 10 | 11 | render() { 12 | return ( 13 | 17 | ); 18 | } 19 | } 20 | 21 | function mapStateToProps(state) { 22 | return { 23 | people: state.falcor.people 24 | }; 25 | } 26 | 27 | export default connect( 28 | mapStateToProps, 29 | )(reduxFalcor(AppContainer)); 30 | -------------------------------------------------------------------------------- /test/integration/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { FalcorProvider } from '../../src/index'; 4 | import { falcor } from '../src/model'; 5 | import {store} from './store'; 6 | import AppContainer from './appContainer'; 7 | 8 | 9 | const main = ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | export default main; 18 | -------------------------------------------------------------------------------- /test/integration/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux'; 2 | import { reducer as falcorReducer } from '../../src/index.js'; 3 | 4 | const reducers = combineReducers({ 5 | falcor: falcorReducer 6 | }); 7 | 8 | export const store = createStore(reducers); 9 | -------------------------------------------------------------------------------- /test/root.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | 3 | function noop() {} 4 | 5 | before(function() { 6 | // disable react warnings! 7 | console.error = noop; 8 | // load jsdom 9 | const doc = jsdom.jsdom(''); 10 | global.document = doc; 11 | global.window = doc.defaultView; 12 | global.navigator = window.navigator; 13 | }); 14 | -------------------------------------------------------------------------------- /test/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class App extends React.Component { 4 | 5 | constructor() { 6 | super(); 7 | this.fetchFalcorDeps = this.fetchFalcorDeps.bind(this); 8 | } 9 | 10 | fetchFalcorDeps() { 11 | return Promise.resolve(); 12 | } 13 | 14 | render() { 15 | return ( 16 |
My favorite movie is {this.props.movie}. - {this.props.name}
17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/src/model.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'falcor'; 2 | 3 | const cache = { 4 | people: [{ 5 | name: 'Dan', 6 | movie: 'Redux' 7 | }] 8 | }; 9 | 10 | const falcor = new Model({ 11 | cache 12 | }); 13 | 14 | export { 15 | cache, 16 | falcor 17 | }; 18 | -------------------------------------------------------------------------------- /test/test_create_store.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { mock, spy } from 'sinon'; 3 | import createStore from '../src/components/createStore.js'; 4 | 5 | describe('createStore', function() { 6 | const reduxStore = { 7 | dispatch() {} 8 | }; 9 | 10 | it('include subscribe and trigger', function() { 11 | const result = createStore(); 12 | assert.ok(result); 13 | assert.ok(result.subscribe); 14 | assert.ok(result.trigger); 15 | }); 16 | 17 | it('uses redux store for dispatch', function() { 18 | const reduxStoreMock = mock(reduxStore); 19 | reduxStoreMock.expects('dispatch').once(); 20 | const result = createStore(reduxStore); 21 | result.trigger('cache'); 22 | reduxStoreMock.verify(); 23 | }); 24 | 25 | it('returns cache', function() { 26 | const store = createStore(reduxStore); 27 | const cache = { message: 'Test' }; 28 | const result = store.trigger(cache); 29 | assert.equal(result, cache); 30 | }); 31 | 32 | it('returns an unsubscribe function', function() { 33 | const store = createStore(); 34 | const fn = store.subscribe(); 35 | assert.equal(typeof fn, 'function'); 36 | assert.equal(fn.name, 'unsubscribe'); 37 | }); 38 | 39 | it('is called when trigger is invoked', function() { 40 | const store = createStore(reduxStore); 41 | const listener = spy(); 42 | store.subscribe(listener); 43 | store.trigger('cache'); 44 | assert(listener.calledOnce); 45 | }); 46 | 47 | it('has a listener that unsubscribes', function() { 48 | const store = createStore(reduxStore); 49 | const listener = spy(); 50 | const unsubscribe = store.subscribe(listener); 51 | unsubscribe(); 52 | store.trigger('cache'); 53 | assert(!listener.called); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/test_duck.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import reduxFalcorReducer, { update } from '../src/components/duck.js'; 3 | 4 | describe('duck', function() { 5 | describe('update', function() { 6 | it('has the right type', function() { 7 | const action = update(); 8 | assert.equal('redux-falcor/UPDATE', action.type); 9 | }); 10 | 11 | it('has the payload', function() { 12 | const cache = { 13 | name: 'Luke Skywalker', 14 | movie: 'Star Wars' 15 | }; 16 | const action = update(cache); 17 | assert.equal(action.payload, cache); 18 | }); 19 | }); 20 | 21 | describe('reduxFalcorReducer', function() { 22 | describe('default action', function() { 23 | it('should have type', function() { 24 | assert.throws(() => { 25 | reduxFalcorReducer(); 26 | }); 27 | }); 28 | 29 | it('should initialize state', function() { 30 | const state = reduxFalcorReducer(undefined, { type: 'INIT' }); 31 | assert.deepEqual(state, {}); 32 | }); 33 | }); 34 | 35 | describe('update action', function() { 36 | it('returns payload', function() { 37 | const payload = { 38 | name: 'Luke Skywalker', 39 | movie: 'Star Wars' 40 | }; 41 | const action = { 42 | type: 'redux-falcor/UPDATE', 43 | payload 44 | }; 45 | const state = reduxFalcorReducer(undefined, action); 46 | assert.deepEqual(state, payload); 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/test_falcor_provider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import assert from 'assert'; 3 | import { Model } from 'falcor'; 4 | import { shallow, mount } from 'enzyme'; 5 | import { mock, spy, stub } from 'sinon'; 6 | import { FalcorProvider } from '../src/index'; 7 | import App from './src/app'; 8 | import { cache } from './src/model'; 9 | 10 | function noop() {} 11 | 12 | describe('FalcorProvider', function() { 13 | describe('initialization', function() { 14 | it('requires falcor prop', function() { 15 | assert.throws(() => shallow()); 16 | }); 17 | 18 | it('should have one child', function() { 19 | const falcor = new Model({ cache }); 20 | assert.throws(() => shallow(), /Invariant Violation:/); 21 | }); 22 | 23 | it('requires redux store', function() { 24 | assert.throws(() => shallow()); 25 | }); 26 | }); 27 | 28 | describe('changing a model', function() { 29 | let store; 30 | let component; 31 | let wrapper; 32 | let falcor; 33 | 34 | before(function() { 35 | store = { dispatch: noop }; 36 | falcor = new Model({ cache }); 37 | component = ; 38 | wrapper = shallow(component); 39 | }); 40 | 41 | it('renders child', function() { 42 | assert.ok(wrapper.some('App')); 43 | }); 44 | 45 | it('gets the falcor model', function(done) { 46 | const falcorMock = mock(falcor); 47 | falcorMock.expects('getCache').once(); 48 | falcor._root.onChange(); 49 | setTimeout(() => { 50 | falcorMock.verify(); 51 | falcorMock.restore(); 52 | done(); 53 | }, 60); 54 | }); 55 | 56 | it('dispatches to the store', function(done) { 57 | const storeMock = mock(store); 58 | storeMock.expects('dispatch').once(); 59 | falcor._root.onChange(); 60 | setTimeout(() => { 61 | storeMock.verify(); 62 | storeMock.restore(); 63 | done(); 64 | }, 60); 65 | }); 66 | }); 67 | 68 | describe('mount', function() { 69 | it('sets the context', function() { 70 | const store = { dispatch: noop }; 71 | const falcor = new Model({ cache }); 72 | const component = ; 73 | const providerMock = mock(FalcorProvider.prototype); 74 | providerMock.expects('getChildContext').once(); 75 | const wrapper = mount(component); 76 | providerMock.verify(); 77 | providerMock.restore(); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/test_integration.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import assert from 'assert'; 3 | import { shallow, mount } from 'enzyme'; 4 | import Main from './integration/main'; 5 | import { cache, falcor } from './src/model'; 6 | 7 | describe('integration', function() { 8 | let wrapper; 9 | 10 | before(function() { 11 | wrapper = mount(Main); 12 | }); 13 | 14 | describe('mount', function() { 15 | it('runs without exceptions', function(){ 16 | assert.ok(wrapper); 17 | }); 18 | 19 | it('renders cache', function(done) { 20 | falcor._root.onChange(); 21 | setTimeout(() => { 22 | const elements = wrapper.find('span'); 23 | assert.equal(elements.length, 2); 24 | assert.equal(elements.at(0).text(), cache.people[0].movie); 25 | assert.equal(elements.at(1).text(), cache.people[0].name); 26 | done(); 27 | }, 60); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/test_redux_falcor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import assert from 'assert'; 3 | import { shallow, mount } from 'enzyme'; 4 | import { mock, spy, stub } from 'sinon'; 5 | import { reduxFalcor } from '../src/index'; 6 | import App from './src/app'; 7 | 8 | function noop() {} 9 | 10 | describe('reduxFalcor', function() { 11 | describe('initialization', function() { 12 | it('throws if no wrappable component', function() { 13 | assert.throws(() => reduxFalcor()); 14 | }); 15 | 16 | it('wraps component', function() { 17 | const wrapper = reduxFalcor(App); 18 | assert.ok(wrapper); 19 | assert.equal(wrapper.displayName, 'ReduxFalcor(App)'); 20 | assert.equal(wrapper.WrappedComponent, App); 21 | }); 22 | }); 23 | 24 | describe('shallow render', function() { 25 | let wrapper; 26 | 27 | before(function() { 28 | wrapper = reduxFalcor(App); 29 | }); 30 | 31 | it('reduxFalcor element is created', function() { 32 | const element = React.createElement(wrapper); 33 | assert.ok(element); 34 | }); 35 | 36 | it('reduxFalcor element requires store', function() { 37 | const element = React.createElement(wrapper); 38 | assert.throws(() => shallow(element), /Invariant Violation:/); 39 | }); 40 | 41 | it('reduxFalcor warns when no context', function() { 42 | console.error = spy(); 43 | const element = React.createElement(wrapper, { falcorStore: {}, falcor: {} }); 44 | const node = shallow(element); 45 | assert.ok(console.error.calledTwice); 46 | console.error = noop; 47 | }); 48 | 49 | it('reduxFalcor element is rendered', function() { 50 | const element = React.createElement(wrapper, { falcorStore: {}, falcor: {} }); 51 | const node = shallow(element); 52 | assert.ok(node.some('App')); 53 | }); 54 | 55 | it('reduxFalcor element has falcor prop', function() { 56 | const falcor = {}; 57 | const element = React.createElement(wrapper, { falcorStore: {}, falcor }); 58 | const node = shallow(element); 59 | const appNode = node.find('App').first(); 60 | assert.equal(appNode.props().falcor, falcor); 61 | }); 62 | }); 63 | 64 | describe('mount', function() { 65 | let wrapper; 66 | let falcorStore; 67 | 68 | before(function() { 69 | wrapper = reduxFalcor(App); 70 | falcorStore = { 71 | subscribe() {} 72 | }; 73 | }); 74 | 75 | it('reduxFalcor is mounted', function() { 76 | const element = React.createElement(wrapper, { falcorStore, falcor: {} }); 77 | const node = mount(element); 78 | assert.ok(node); 79 | }); 80 | 81 | it('reduxFalcor subscribes to the store on mounting', function() { 82 | const falcorStoreMock = mock(falcorStore); 83 | falcorStoreMock.expects('subscribe').once(); 84 | const element = React.createElement(wrapper, { falcorStore, falcor: {} }); 85 | const node = mount(element); 86 | falcorStoreMock.verify(); 87 | falcorStoreMock.restore(); 88 | }); 89 | 90 | it('reduxFalcor calls fetchFalcorDeps on mounting', function() { 91 | const appMock = mock(App.prototype); 92 | appMock.expects('fetchFalcorDeps').returns(Promise.resolve()).once(); 93 | const falcorStoreStub = stub(falcorStore, 'subscribe').returns(noop); 94 | const element = React.createElement(wrapper, { falcorStore, falcor: {} }); 95 | const node = mount(element); 96 | appMock.verify(); 97 | appMock.restore(); 98 | falcorStoreStub.restore(); 99 | }); 100 | 101 | it('reduxFalcor unsubscribes on unmounting', function() { 102 | const unsubscribe = spy(); 103 | const falcorStoreStub = stub(falcorStore, 'subscribe').returns(unsubscribe); 104 | const element = React.createElement(wrapper, { falcorStore, falcor: {} }); 105 | const node = mount(element); 106 | // unmount causes an exception, see Enzyme open issues. 107 | assert.throws(() => node.unmount()); 108 | assert.ok(unsubscribe.calledOnce); 109 | falcorStoreStub.restore(); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | var reactExternal = { 6 | root: 'React', 7 | commonjs2: 'react', 8 | commonjs: 'react', 9 | amd: 'react' 10 | } 11 | 12 | module.exports = { 13 | externals: { 14 | 'react': reactExternal 15 | }, 16 | module: { 17 | loaders: [ 18 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ } 19 | ] 20 | }, 21 | output: { 22 | library: 'ReduxFalcor', 23 | libraryTarget: 'umd' 24 | }, 25 | resolve: { 26 | extensions: ['', '.js'] 27 | }, 28 | plugins: [ 29 | new webpack.optimize.OccurenceOrderPlugin() 30 | ] 31 | }; 32 | --------------------------------------------------------------------------------