├── .eslintignore ├── .gitignore ├── lib ├── defaultFluxKey.js ├── util │ ├── isReactComponent.js │ ├── shouldPureComponentUpdate.js │ ├── entriesToObject.js │ ├── __tests__ │ │ ├── creatable.js │ │ ├── diff.js │ │ └── isReactComponent.js │ ├── creatable.js │ └── diff.js ├── __tests__ │ ├── fixtures │ │ ├── components │ │ │ ├── App.jsx │ │ │ ├── User.jsx │ │ │ └── Users.jsx │ │ ├── RenderClient.js │ │ ├── startServersAndBrowse.js │ │ ├── ComplexClass.js │ │ ├── autobind.js │ │ ├── pageTemplate.js │ │ ├── createFlux.js │ │ ├── RenderServer.js │ │ └── ApiServer.js │ ├── node │ │ ├── index.js │ │ ├── components.js │ │ ├── preparable.js │ │ ├── stores.js │ │ ├── app.js │ │ └── prepare.js │ └── browser │ │ ├── usersVisibility.js │ │ ├── deleteUser.js │ │ ├── updateUser.js │ │ ├── createUser.js │ │ └── renderedPage.js ├── Routable.js ├── index.js ├── Action.js ├── HTTPStore │ ├── headersToObject.js │ └── index.js ├── deps.js ├── preparable.js ├── root.js ├── actions.js ├── stores.js ├── prepare.js ├── Store.js ├── MemoryStore │ └── index.js └── Flux.js ├── config ├── babel │ ├── node │ │ ├── index.js │ │ ├── dev │ │ │ └── index.js │ │ └── prod │ │ │ └── index.js │ ├── browser │ │ ├── index.js │ │ ├── dev │ │ │ └── index.js │ │ └── prod │ │ │ └── index.js │ └── index.js ├── gulp │ ├── tasks │ │ ├── default.js │ │ ├── lint.js │ │ ├── clean.js │ │ ├── test.js │ │ ├── build.js │ │ └── selenium.js │ └── index.js ├── jsdom │ └── setup.js └── wdio │ ├── dev │ └── wdio.conf.js │ └── prod │ └── wdio.conf.js ├── .npmignore ├── .editorconfig ├── gulpfile.js ├── webpackConfig.jsx ├── README.md ├── package.json └── .eslintrc /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.pid 3 | *.lock 4 | npm-debug.log 5 | dist 6 | -------------------------------------------------------------------------------- /lib/defaultFluxKey.js: -------------------------------------------------------------------------------- 1 | // Guaranteed to be unique by fair dice roll. (see RFC 1149.5) 2 | export default 'defaultFluxKey_f8KfYJm6kWsX'; 3 | -------------------------------------------------------------------------------- /config/babel/node/index.js: -------------------------------------------------------------------------------- 1 | import dev from './dev'; 2 | import prod from './prod'; 3 | 4 | export default { 5 | dev, 6 | prod, 7 | }; 8 | -------------------------------------------------------------------------------- /config/babel/browser/index.js: -------------------------------------------------------------------------------- 1 | import dev from './dev'; 2 | import prod from './prod'; 3 | 4 | export default { 5 | dev, 6 | prod, 7 | }; 8 | -------------------------------------------------------------------------------- /config/babel/index.js: -------------------------------------------------------------------------------- 1 | import browser from './browser'; 2 | import node from './node'; 3 | 4 | export default { 5 | browser, 6 | node, 7 | }; 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.pid 3 | *.lock 4 | npm-debug.log 5 | /app 6 | /config 7 | /gulpfile.js 8 | /README.md 9 | !dist 10 | !package.json 11 | -------------------------------------------------------------------------------- /config/gulp/tasks/default.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import runSequence from 'run-sequence'; 3 | 4 | export default () => 5 | gulp.task('default', (cb) => runSequence('clean', 'test', cb)) 6 | ; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | [*] 3 | insert_final_newline = true 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | [*.md] 9 | trim_trailing_whitespace = false 10 | -------------------------------------------------------------------------------- /lib/util/isReactComponent.js: -------------------------------------------------------------------------------- 1 | export default function isReactCompositeComponent(component) { 2 | if(typeof component.prototype === 'object' && component.prototype.isReactComponent) { 3 | return true; 4 | } 5 | return false; 6 | } 7 | -------------------------------------------------------------------------------- /lib/util/shouldPureComponentUpdate.js: -------------------------------------------------------------------------------- 1 | import shallowEqual from 'shallowequal'; 2 | 3 | function shouldPureComponentUpdate(nextProps, nextState) { 4 | return (!shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState)); 5 | } 6 | 7 | export default shouldPureComponentUpdate; 8 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | require('source-map-support/register'); 3 | require('babel-register')({ 4 | only: ['config'], 5 | presets: [path.resolve(__dirname, './config/babel/node/dev')], 6 | retainLines: true, 7 | }); 8 | require('babel-polyfill'); 9 | require('./config/gulp'); 10 | -------------------------------------------------------------------------------- /lib/util/entriesToObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an object from an array of key/value pairs. 3 | * @param {Array} entries Pairs of key/value 4 | * @return {Object} Resulting object 5 | */ 6 | export default function entriesToObject(entries) { 7 | const o = {}; 8 | for(const [k, v] of entries) { 9 | o[k] = v; 10 | } 11 | return o; 12 | } 13 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { root } from '../../..'; 4 | import Users from './Users'; 5 | 6 | export default root()(class App extends React.Component { 7 | static displayName = 'App'; 8 | 9 | render() { 10 | return
11 | 12 |
; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /config/jsdom/setup.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom').jsdom; 2 | 3 | global.document = jsdom(''); 4 | global.window = document.defaultView; 5 | Object.keys(document.defaultView).forEach((property) => { 6 | if (typeof global[property] === 'undefined') { 7 | global[property] = document.defaultView[property]; 8 | } 9 | }); 10 | 11 | global.navigator = { 12 | userAgent: 'node.js' 13 | }; 14 | -------------------------------------------------------------------------------- /config/gulp/index.js: -------------------------------------------------------------------------------- 1 | import registerBuild from './tasks/build'; 2 | import registerClean from './tasks/clean'; 3 | import registerDefault from './tasks/default'; 4 | import registerLint from './tasks/lint'; 5 | import registerSelenium from './tasks/selenium'; 6 | import registerTest from './tasks/test'; 7 | 8 | registerClean(); 9 | registerLint(); 10 | registerTest(); 11 | registerBuild(); 12 | registerSelenium(); 13 | 14 | registerDefault(); 15 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/RenderClient.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './components/App'; 5 | import createFlux from './createFlux'; 6 | const { port } = JSON.parse(new Buffer(window.__HTTP_CONFIG__, 'base64').toString('utf8')); 7 | const reflux = createFlux({ port }) 8 | .loadState(JSON.parse(new Buffer(window.__NEXUS_PAYLOAD__, 'base64').toString('utf8'))); 9 | ReactDOM.render(, document.getElementById('__App__')); 10 | -------------------------------------------------------------------------------- /lib/Routable.js: -------------------------------------------------------------------------------- 1 | import pathToRegexp from 'path-to-regexp'; 2 | 3 | /** 4 | * Represents a routable Object 5 | * @abstract 6 | */ 7 | class Routable { 8 | /** 9 | * Constructs a new Routable instance. 10 | * @constructor 11 | * @param {String} route Route of the routable. 12 | */ 13 | constructor(route) { 14 | this.route = route; 15 | this.keys = []; 16 | this.re = pathToRegexp(route, this.keys); 17 | this.toPath = pathToRegexp.compile(this.route); 18 | } 19 | } 20 | 21 | export default Routable; 22 | -------------------------------------------------------------------------------- /lib/util/__tests__/creatable.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = global; 2 | import should from 'should/as-function'; 3 | 4 | import creatable from '../creatable'; 5 | describe('creatable', () => { 6 | it('decorates class with a create static method which acts as a factory', () => { 7 | @creatable 8 | class A { 9 | constructor(foo) { 10 | this.foo = foo; 11 | } 12 | } 13 | const a = A.create('bar'); 14 | should(a).be.an.instanceOf(A).which.has.property('foo').which.is.exactly('bar'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /config/gulp/tasks/lint.js: -------------------------------------------------------------------------------- 1 | import eslint from 'gulp-eslint'; 2 | import gulp from 'gulp'; 3 | import path from 'path'; 4 | import plumber from 'gulp-plumber'; 5 | 6 | const jsFilesGlob = path.join( 7 | __dirname, // /config/gulp/tasks 8 | '..', // /config/gulp 9 | '..', // /config/ 10 | '..', // / 11 | 'lib', // /lib/ 12 | '**', 13 | '*.js', 14 | ); 15 | 16 | export default () => 17 | gulp.task('lint', () => gulp.src(jsFilesGlob) 18 | .pipe(plumber()) 19 | .pipe(eslint()) 20 | .pipe(eslint.format()) 21 | ) 22 | ; 23 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | export { default as Action } from './Action'; 4 | export { default as actions } from './actions'; 5 | export { default as deps } from './deps'; 6 | export { default as Flux } from './Flux'; 7 | export { default as preparable } from './preparable'; 8 | export { default as prepare } from './prepare'; 9 | export { default as root } from './root'; 10 | export { default as Store } from './Store'; 11 | export { default as stores } from './stores'; 12 | export { default as HTTPStore } from './HTTPStore'; 13 | export { default as MemoryStore } from './MemoryStore'; 14 | -------------------------------------------------------------------------------- /lib/Action.js: -------------------------------------------------------------------------------- 1 | import Routable from './Routable'; 2 | import creatable from './util/creatable'; 3 | 4 | /** 5 | * Class representing an action. 6 | * @extends Routable 7 | */ 8 | @creatable 9 | class Action extends Routable { 10 | /** 11 | * Creates an Action. 12 | * @constructor 13 | * @param {String} route Route of the action 14 | * @param {Function} dispatch Function to call when the action is dispatched 15 | */ 16 | constructor(route, dispatch) { 17 | super(route); 18 | this.dispatch = (...args) => Reflect.apply(dispatch, this, args); 19 | } 20 | } 21 | 22 | export default Action; 23 | -------------------------------------------------------------------------------- /lib/util/creatable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creatable class decorator. 3 | * Adds a static `create` method which as a static factory, taking the same params as the constructor. 4 | * @param {Function} Class The Class to be marked as abstract 5 | * @return {Function} Class The decorated Class 6 | */ 7 | function creatable(Class) { 8 | if(typeof Class !== 'function') { 9 | throw new TypeError('@creatable should only be applied to classes.'); 10 | } 11 | return class CreatableClass extends Class { 12 | static create(...args) { 13 | return new this(...args); 14 | } 15 | }; 16 | } 17 | 18 | export default creatable; 19 | -------------------------------------------------------------------------------- /lib/HTTPStore/headersToObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracts headers from the given headers according his type into an Object. 3 | * 4 | * @param {Object} headers The headers to extract 5 | * @throws {Error} An error throws if the headers cannot be extracted 6 | * @return {Object} The extracted headers 7 | */ 8 | function headersToObject(headers) { 9 | if(typeof headers.raw === 'function') { 10 | return headers.raw(); 11 | } 12 | if(typeof headers.forEach === 'function') { 13 | const o = {}; 14 | headers.forEach((v, k) => o[k] = v); 15 | return o; 16 | } 17 | throw new Error('Could not find a suitable interface for converting headers to object'); 18 | } 19 | 20 | export default headersToObject; 21 | -------------------------------------------------------------------------------- /config/gulp/tasks/clean.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import path from 'path'; 3 | import rimraf from 'rimraf'; 4 | 5 | import babelConfig from '../../babel'; 6 | 7 | const dist = path.join( 8 | __dirname, // /config/gulp/tasks 9 | '..', // /config/gulp 10 | '..', // /config/ 11 | '..', // / 12 | 'dist', // /dist 13 | ); 14 | 15 | export default () => { 16 | const platforms = Object.keys(babelConfig); 17 | platforms.forEach((platform) => { 18 | const envs = Object.keys(babelConfig[platform]); 19 | envs.forEach((env) => 20 | gulp.task(`clean-${platform}-${env}`, (cb) => rimraf(path.join(dist, platform, env), cb)) 21 | ); 22 | }); 23 | gulp.task('clean', (cb) => rimraf(dist, cb)); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/startServersAndBrowse.js: -------------------------------------------------------------------------------- 1 | import ApiServer from './ApiServer'; 2 | import RenderServer from './RenderServer'; 3 | 4 | async function startServersAndBrowse(browser) { 5 | const apiServer = new ApiServer({ port: 0 }); 6 | await apiServer.startListening(); 7 | const { port: apiPort } = apiServer.server.address(); 8 | 9 | const renderServer = new RenderServer({ port: 0, apiPort }); 10 | await renderServer.startListening(); 11 | const { port: renderPort } = renderServer.server.address(); 12 | await browser.url(`http://localhost:${renderPort}`); 13 | 14 | return () => 15 | Promise.all([renderServer.stopListening(), apiServer.stopListening()]); 16 | } 17 | 18 | export default startServersAndBrowse; 19 | -------------------------------------------------------------------------------- /lib/util/__tests__/diff.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = global; 2 | import should from 'should/as-function'; 3 | 4 | import diff from '../diff'; 5 | 6 | describe('diff', () => { 7 | it('returns no-op diff for identical objects', () => { 8 | should(diff({}, {})).be.deepEqual([{}, {}]); 9 | should(diff( 10 | { foo: 'bar' }, 11 | { foo: 'bar' }, 12 | )).be.deepEqual([{}, {}]); 13 | }); 14 | it('returns correct diff for different objects', () => { 15 | should(diff( 16 | { foo: 'bar', fizz: 'bozz', theta: 'alpha' }, // next 17 | { foo: 'bar', fizz: 'buzz', beta: 'gamma' }, // prev 18 | )).be.deepEqual([ 19 | { fizz: 'buzz', beta: 'gamma' }, // removed 20 | { fizz: 'bozz', theta: 'alpha' }, // added 21 | ]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/ComplexClass.js: -------------------------------------------------------------------------------- 1 | import autobind from './autobind'; 2 | 3 | class Base { 4 | constructor(v) { 5 | this.v = v; 6 | } 7 | } 8 | 9 | function multiply(by) { 10 | return function $multiply(target, name, descriptor) { 11 | return { 12 | ...descriptor, 13 | value(...args) { 14 | return by * Reflect.apply(descriptor.value, this, args); 15 | }, 16 | }; 17 | }; 18 | } 19 | 20 | const FIVE = 5; 21 | const SEVEN = 7; 22 | 23 | @autobind 24 | class Complex extends Base { 25 | @multiply(SEVEN) 26 | static multiplyByFortyFive(v) { 27 | return FIVE * v; 28 | } 29 | 30 | @multiply(1 / SEVEN) 31 | multiplyByFortyFive() { 32 | return SEVEN * this.constructor.multiplyByFortyFive(this.v); 33 | } 34 | } 35 | 36 | const x = Complex; 37 | 38 | export default x; 39 | -------------------------------------------------------------------------------- /lib/util/__tests__/isReactComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isReactComponent from '../isReactComponent'; 3 | import should from 'should/as-function'; 4 | const { describe, it } = global; 5 | 6 | describe('isReactComponent(type)', () => { 7 | it('returns true on React.Component', () => { 8 | class C extends React.Component { 9 | render() { 10 | return null; 11 | } 12 | } 13 | should(isReactComponent(C)).be.true(); 14 | }); 15 | it('returns true on React.PureComponent', () => { 16 | class C extends React.PureComponent { 17 | render() { 18 | return null; 19 | } 20 | } 21 | should(isReactComponent(C)).be.true(); 22 | }); 23 | it('returns false on a functional component', () => { 24 | const C = () => (
{'foo'}
); 25 | should(isReactComponent(C)).be.false(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /lib/deps.js: -------------------------------------------------------------------------------- 1 | import decorateAction from './actions'; 2 | import decorateStores from './stores'; 3 | 4 | /** 5 | * Enhance a React Component and make context's {@link Flux}'s {@link Actions}s and {@link Stores} requested by bindings 6 | * avaliable as props. 7 | * @param {Function} getDeps Function taking component props and returning an object containing 8 | * Action and Store bindings for the component. 9 | * @param {Object} options Options object to provide for stores and actions. 10 | * @return {Component} Enhanced component. 11 | * @example 12 | * ... 13 | * @deps((props) => ({ 14 | * actions: { 15 | * ... 16 | * }, 17 | * stores: { 18 | * ... 19 | * }, 20 | * }), { 21 | * actions: { 22 | * ... 23 | * }, 24 | * stores: { 25 | * ... 26 | * }, 27 | * }) 28 | * class Foo extends Component { 29 | * ... 30 | */ 31 | export default function deps(getDeps, options = {}) { 32 | return (Component) => 33 | decorateAction((props) => getDeps(props).actions, options.actions)( 34 | decorateStores((props) => getDeps(props).stores, options.stores)( 35 | Component, 36 | ), 37 | ) 38 | ; 39 | } 40 | -------------------------------------------------------------------------------- /lib/preparable.js: -------------------------------------------------------------------------------- 1 | import isReactComponent from './util/isReactComponent'; 2 | const $prepare = Symbol('preparable'); 3 | 4 | /** 5 | * Decorate a React.Component to make it preparable by the prepare() function. 6 | * @param {Function} prepare Async function which takes props and returns a Promise for when the component is ready 7 | * to be rendered. 8 | * @return {Function} A function which takes a React.Component and returns a preparable version 9 | */ 10 | function preparable(prepare) { 11 | if(typeof prepare !== 'function') { 12 | throw new TypeError('@preparable() should be passed an async function'); 13 | } 14 | return function extendComponent(Component) { 15 | if(!isReactComponent(Component)) { 16 | throw new TypeError('@preparable should only be applied to React Components'); 17 | } 18 | return class extends Component { 19 | static async [$prepare](props, context) { 20 | if(Component[$prepare]) { 21 | await Component[$prepare](props, context); 22 | } 23 | return await prepare(props, context); 24 | } 25 | }; 26 | }; 27 | } 28 | 29 | Object.assign(preparable, { $prepare }); 30 | 31 | export default preparable; 32 | -------------------------------------------------------------------------------- /webpackConfig.jsx: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | 3 | import babelConfig from './config/babel'; 4 | const webpackConfig = (env) => ({ 5 | target: 'web', 6 | debug: env === 'prod', 7 | devtool: 'eval', 8 | module: { 9 | noParse: ['/^fb$/'], 10 | loaders: [ 11 | { 12 | test: /\.json$/, 13 | exclude: /node_modules/, 14 | loader: 'json-loader', 15 | }, 16 | { 17 | test: /\.js[x]?$/, 18 | exclude: /node_modules/, 19 | loader: 'babel-loader', 20 | query: { 21 | ignore: ['node_modules', 'dist'], 22 | plugins: babelConfig.browser[env].plugins, 23 | }, 24 | }, 25 | ], 26 | }, 27 | plugins: [ 28 | new webpack.DefinePlugin({ 29 | 'process.env': { 30 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 31 | }, 32 | }), 33 | new webpack.ProvidePlugin({ 34 | 'Promise': 'bluebird', 35 | }), 36 | new webpack.optimize.DedupePlugin(), 37 | ], 38 | node: { 39 | console: true, 40 | fs: 'empty', 41 | net: 'empty', 42 | tls: 'empty', 43 | }, 44 | resolve: { 45 | extensions: ['', '.js', '.jsx'], 46 | }, 47 | }); 48 | export default webpackConfig; 49 | -------------------------------------------------------------------------------- /lib/root.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import Flux from './Flux'; 5 | import defaultFluxKey from './defaultFluxKey'; 6 | 7 | /** 8 | * Defines a function that returns a React Component. 9 | * This React Component will use the React's "context" feature. 10 | * This feature lets to pass data through the component tree, 11 | * without having to pass the props down manually at every level. 12 | * 13 | * @param {Object} config Defines the object that handle `fluxKey` and `displayName`. 14 | * @param {String} config.fluxKey The key to retrieve the flux with React's "context". 15 | * @param {String} config.displayName The displayName of the component. 16 | * @return {Function} The function that will define the component with the React's "context". 17 | */ 18 | function root({ fluxKey = defaultFluxKey, displayName = void 0 } = {}) { 19 | return (Component) => { 20 | class RootComponent extends React.Component { 21 | static displayName = displayName || `@root(${Component.displayName})`; 22 | 23 | static propTypes = { 24 | flux: PropTypes.instanceOf(Flux), 25 | }; 26 | 27 | static childContextTypes = { 28 | [fluxKey]: PropTypes.instanceOf(Flux), 29 | }; 30 | 31 | getChildContext() { 32 | return { 33 | [fluxKey]: this.props.flux, 34 | }; 35 | } 36 | 37 | render() { 38 | return ; 39 | } 40 | } 41 | 42 | return RootComponent; 43 | }; 44 | } 45 | 46 | export default root; 47 | -------------------------------------------------------------------------------- /lib/__tests__/node/index.js: -------------------------------------------------------------------------------- 1 | import should from 'should/as-function'; 2 | const { describe, it } = global; 3 | 4 | import { 5 | Action, 6 | actions, 7 | deps, 8 | Flux, 9 | preparable, 10 | prepare, 11 | root, 12 | Store, 13 | stores, 14 | HTTPStore, 15 | MemoryStore, 16 | } from '../../'; 17 | import ComplexClass from '../fixtures/ComplexClass'; 18 | 19 | describe('sanity', () => { 20 | it('shouldjs should not extend Object.prototype', () => should(Object.prototype).not.have.property('should')); 21 | it('Complex class transforms should work', () => { 22 | const TEN = 10; 23 | const THIRTYFIVE = 35; 24 | const inst = new ComplexClass(TEN); 25 | should(inst).be.an.instanceOf(ComplexClass); 26 | should(inst.v).be.exactly(TEN); 27 | const multiplyByFortyFive = inst.multiplyByFortyFive; 28 | should(multiplyByFortyFive()).be.exactly(TEN * THIRTYFIVE); 29 | should(ComplexClass.multiplyByFortyFive(TEN)).be.exactly(TEN * THIRTYFIVE); 30 | }); 31 | }); 32 | 33 | describe('index', () => 34 | it('should expose correctly the components of the library', () => { 35 | should(Action).be.a.Function(); 36 | should(actions).be.a.Function(); 37 | should(deps).be.a.Function(); 38 | should(Flux).be.a.Function(); 39 | should(preparable).be.a.Function(); 40 | should(prepare).be.a.Function(); 41 | should(root).be.a.Function(); 42 | should(Store).be.a.Function(); 43 | should(stores).be.a.Function(); 44 | should(HTTPStore).be.a.Function(); 45 | should(MemoryStore).be.a.Function(); 46 | }) 47 | ); 48 | -------------------------------------------------------------------------------- /lib/__tests__/node/components.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import stores from '../../stores'; 4 | import Store from '../../Store'; 5 | import MemoryStore from '../../MemoryStore'; 6 | import Flux from '../../Flux'; 7 | import root from '../../root'; 8 | import { mount } from 'enzyme'; 9 | import should from 'should/as-function'; 10 | 11 | const { describe, it } = global; 12 | 13 | describe('stores', () => { 14 | it('should set state to undefined when binding is removed', () => { 15 | const fooStore = new MemoryStore('/foo').set({}, 'foo'); 16 | const flux = Flux.create({ 17 | stores: [fooStore], 18 | }); 19 | class Bar extends Component { 20 | static displayName = 'Bar'; 21 | static propTypes = { 22 | foo: Store.State.propType(PropTypes.string), 23 | }; 24 | render() { 25 | const { foo } = this.props; 26 | if(foo === void 0) { 27 | return {'foo is undefined'}; 28 | } 29 | return {'foo is not undefined'}; 30 | } 31 | } 32 | const DecoratedBar = root()( 33 | stores(({ bindFoo }) => { 34 | if(bindFoo) { 35 | return { foo: '/foo' }; 36 | } 37 | return {}; 38 | })(Bar) 39 | ); 40 | const mounted = mount(); 41 | should(mounted.html()).be.exactly('foo is not undefined'); 42 | mounted.setProps({ bindFoo: false }); 43 | should(mounted.html()).be.exactly('foo is undefined'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /lib/util/diff.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | /** 4 | * Returns the properties of the first object that differ from the second one. 5 | * @param {Object} prev Original objects 6 | * @param {Object} next Object with potential differences 7 | * @param {Function} eql Values comparaison function 8 | * @return {Object} Object composed of `prev` properties which are different form the `next` ones. 9 | */ 10 | function changedProperties(prev, next, eql) { 11 | return _.pickBy(prev, (v, k) => !next.hasOwnProperty(k) || !eql(next[k], v)); 12 | } 13 | 14 | /** 15 | * Compares the two values and returns if the to values are equal (not deeply). 16 | * @param {*} x First value 17 | * @param {*} y Second value 18 | * @return {Boolean} True if {x} is equal to {y}, false otherwise. 19 | */ 20 | const shallowEqual = (x, y) => x === y; 21 | 22 | /** 23 | * Diffs two object hashes, returning an array of two hashes: 24 | * - an object containing the removed properties and their previous values, 25 | * - on object containg the added properties and their new values. 26 | * Mutated properties (properties present in both prev and next but with different values) are present in both objects. 27 | * @param {Object} next The next object 28 | * @param {Object} prev The previous object 29 | * @param {Function?} eql = shallowEqual An optional equality function (defaults to ===) 30 | * @return {Array} An array containing two objects: the removed properties, and the added properties. 31 | */ 32 | function diff(next, prev, eql = shallowEqual) { 33 | return [ 34 | changedProperties(prev, next, eql), 35 | changedProperties(next, prev, eql), 36 | ]; 37 | } 38 | 39 | export default diff; 40 | -------------------------------------------------------------------------------- /config/gulp/tasks/test.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import mocha from 'gulp-mocha'; 3 | import path from 'path'; 4 | import through2 from 'through2'; 5 | import runSequence from 'run-sequence'; 6 | 7 | import babelConfig from '../../babel'; 8 | 9 | const root = path.join( 10 | __dirname, // /config/gulp/tasks 11 | '..', // /config/gulp 12 | '..', // /config/ 13 | '..', // / 14 | ); 15 | 16 | const dist = path.join(root, 'dist'); 17 | 18 | function sync(cb) { 19 | return through2.obj((data, enc, f) => f(), (f) => { f(); cb(); }); 20 | } 21 | 22 | function createTest(platform, env) { 23 | const tests = [ 24 | path.join(dist, platform, env, '**', '__tests__', '**', '*.js'), 25 | `!${path.join(dist, platform, env, '**', '__tests__', 'fixtures', '**', '*.js')}`, 26 | `!${path.join(dist, platform, env, '**', '__tests__', 'browser', '**', '*.js')}`, 27 | ]; 28 | return (cb) => { 29 | gulp.src(tests, { read: false }) 30 | .pipe(mocha({ require: [require.resolve('../../jsdom/setup')] })) 31 | .pipe(sync(cb)); 32 | }; 33 | } 34 | 35 | export default () => { 36 | const platforms = Object.keys(babelConfig); 37 | platforms.forEach((platform) => { 38 | const envs = Object.keys(babelConfig[platform]); 39 | envs.forEach((env) => 40 | gulp.task(`test-${platform}-${env}`, [`build-${platform}-${env}`], createTest(platform, env)) 41 | ); 42 | }); 43 | gulp.task('test-selenium', (cb) => runSequence('test-selenium-dev', 'test-selenium-prod', cb)); 44 | gulp.task('test-browser', (cb) => runSequence('test-browser-dev', 'test-browser-prod', cb)); 45 | gulp.task('test-node', (cb) => runSequence('test-node-dev', 'test-node-prod', cb)); 46 | gulp.task('test', (cb) => runSequence('test-browser', 'test-node', cb)); 47 | }; 48 | -------------------------------------------------------------------------------- /lib/__tests__/browser/usersVisibility.js: -------------------------------------------------------------------------------- 1 | import cheerio from 'cheerio'; 2 | import React from 'react'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import should from 'should/as-function'; 5 | 6 | import startServersAndBrowse from '../fixtures/startServersAndBrowse'; 7 | 8 | const { load } = cheerio; 9 | const TIME_OUT = 250; 10 | 11 | function removeReactAttributes(html) { 12 | return html.replace(/ (data-reactid|data-react-checksum|data-reactroot)=".*?"/g, ''); 13 | } 14 | 15 | const { after, before, describe, it, browser } = global; 16 | describe('[FT] Users Visibility', () => { 17 | 18 | let stopServers = null; 19 | 20 | before(async function $before(done) { 21 | stopServers = await startServersAndBrowse(browser); 22 | done(); 23 | }); 24 | after(async function $after() { 25 | return await stopServers(); 26 | }); 27 | 28 | it('should dispatch UsersVisibility action and check if the users list has been hidden', () => { 29 | const expectedAppHtml = load(ReactDOMServer.renderToStaticMarkup( 30 |
31 |
32 |
33 |
34 | 37 |
38 | 39 | 40 | 41 |
42 |
43 |
)).html(); 44 | return browser 45 | .click('#UsersVisibility') 46 | .pause(TIME_OUT) 47 | .getHTML('#__App__', (err, appHtml) => { 48 | if(err) { 49 | return err; 50 | } 51 | should(load(removeReactAttributes(appHtml)).html()).be.equal(expectedAppHtml); 52 | }); 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/autobind.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 2015, Andrey Popp <8mayday@gmail.com> 3 | * 4 | * The decorator may be used on classes or methods 5 | * ``` 6 | * @autobind 7 | * class FullBound {} 8 | * 9 | * class PartBound { 10 | * @autobind 11 | * method () {} 12 | * } 13 | * ``` 14 | */ 15 | 16 | /* 17 | * Return a descriptor removing the value and returning a getter 18 | * The getter will return a .bind version of the function 19 | * and memoize the result against a symbol on the instance 20 | */ 21 | function boundMethod(target, key, descriptor) { 22 | const fn = descriptor.value; 23 | 24 | if(typeof fn !== 'function') { 25 | throw new Error(`@autobind decorator can only be applied to methods not: ${typeof fn}`); 26 | } 27 | 28 | return { 29 | configurable: true, 30 | get() { 31 | if (this === target.prototype) { 32 | return fn; 33 | } 34 | const boundFn = fn.bind(this); 35 | Reflect.defineProperty(this, key, { 36 | value: boundFn, 37 | configurable: true, 38 | writable: true, 39 | }); 40 | return boundFn; 41 | }, 42 | }; 43 | } 44 | 45 | /* 46 | * Use boundMethod to bind all methods on the target.prototype 47 | */ 48 | function boundClass(target) { 49 | // (Using reflect to get all keys including symbols) 50 | const keys = Reflect.ownKeys(target.prototype); 51 | 52 | keys.forEach((key) => { 53 | // Ignore special case target method 54 | if (key === 'constructor') { 55 | return; 56 | } 57 | 58 | const descriptor = Reflect.getOwnPropertyDescriptor(target.prototype, key); 59 | 60 | // Only methods need binding 61 | if (typeof descriptor.value === 'function') { 62 | Reflect.defineProperty(target.prototype, key, boundMethod(target, key, descriptor)); 63 | } 64 | }); 65 | return target; 66 | } 67 | 68 | export default function autobind(...args) { 69 | if(args.length === 1) { 70 | return boundClass(...args); 71 | } 72 | return boundMethod(...args); 73 | } 74 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/pageTemplate.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import _ from 'lodash'; 5 | 6 | // These files will only be read once at startup time. 7 | // It is safe to read them synchronously. 8 | const js = fs.readFileSync( // eslint-disable-line no-sync 9 | path.join( 10 | path.resolve(__dirname, '..', '..', '..'), 11 | path.join('static', 'c.js'), 12 | ) 13 | ); 14 | 15 | // CSS and JS are injected inline. 16 | // This might change in the future. 17 | const tpl = _.template(` 18 | 19 | 20 | 21 | 22 | 23 | <%- title %> 24 | 25 | 26 | 27 | 28 | 29 |
<%= appHtml %>
30 | 31 | 32 | 33 | 34 | 35 | `); 36 | 37 | /** 38 | * Returns the HTML to send to the client 39 | * @param {Object} data Data to bind to the template 40 | * @param {String} options.title Title of the page 41 | * @param {String} options.appHtml Raw HTML to put inside the app div container 42 | * @param {String} options.nexusPayload Payload exported from preparing the app 43 | * @return {String} HTML to safely send directly to the client 44 | */ 45 | function template({ title, appHtml, nexusPayload, httpConfig }) { 46 | return tpl({ 47 | title, 48 | appHtml, 49 | js, 50 | nexusPayload: 51 | // base64 encode to obfuscate URLs and prevent injection 52 | `window.__NEXUS_PAYLOAD__="${new Buffer(nexusPayload).toString('base64')}"`, 53 | httpConfig: 54 | // base64 encode to obfuscate URLs and prevent injection 55 | `window.__HTTP_CONFIG__='${new Buffer(httpConfig).toString('base64')}'`, 56 | }); 57 | } 58 | 59 | export default template; 60 | -------------------------------------------------------------------------------- /lib/__tests__/node/preparable.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = global; 2 | import Promise from 'bluebird'; 3 | import React from 'react'; 4 | import should from 'should/as-function'; 5 | import sinon from 'sinon'; 6 | 7 | import preparable from '../../preparable'; 8 | const { $prepare } = preparable; 9 | 10 | describe('preparable', () => { 11 | it('returns a React.Component with a [$prepare] static property', () => { 12 | class Test extends React.Component {} 13 | const PreparableTest = preparable(() => Promise.resolve())(Test); 14 | should(PreparableTest).have.property($prepare).which.is.a.Function(); 15 | }); 16 | it('static method settles correctly', async function test() { 17 | const delay = 100; 18 | const spy = sinon.spy(); 19 | class Test extends React.Component {} 20 | const PreparableTest = preparable(async function prepare() { 21 | await Promise.resolve(spy()).delay(delay); 22 | })(Test); 23 | await PreparableTest[$prepare](); 24 | should(spy).have.property('calledOnce').which.is.exactly(true); 25 | }); 26 | it('preparable decorator static method settles correctly', async function test() { 27 | const delay = 100; 28 | const spy = sinon.spy(); 29 | @preparable(async function prepare() { 30 | await Promise.resolve(spy()).delay(delay); 31 | }) 32 | class Test extends React.Component {} 33 | await Test[$prepare](); 34 | should(spy).have.property('calledOnce').which.is.exactly(true); 35 | }); 36 | it('correctly chains preparations', async function test() { 37 | const EXPECTED_CALL_COUNT = 2; 38 | const delay = 100; 39 | const spy = sinon.spy(); 40 | @preparable(async function testA() { 41 | await Promise.resolve(spy('A')).delay(delay); 42 | }) 43 | @preparable(async function testB() { 44 | await Promise.resolve(spy('B')).delay(delay); 45 | }) 46 | class Test extends React.Component {} 47 | await Test[$prepare](); 48 | should(spy).have.property('callCount').which.is.exactly(EXPECTED_CALL_COUNT); 49 | should(spy.getCall(0).args).be.deepEqual(['B']); 50 | should(spy.getCall(1).args).be.deepEqual(['A']); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/createFlux.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | 3 | import fetch from 'isomorphic-fetch'; 4 | 5 | import { Flux, MemoryStore, HTTPStore, Action } from '../../'; 6 | 7 | const action = (...args) => Action.create(...args); 8 | const storeMemory = (...args) => MemoryStore.create(...args); 9 | const storeHTTP = (...args) => HTTPStore.create(...args); 10 | 11 | async function request(httpConfig, method, pathname, data) { 12 | const uri = url.format( 13 | Object.assign( 14 | {}, 15 | httpConfig, 16 | { pathname }, 17 | ) 18 | ); 19 | return await fetch(uri, { 20 | method, 21 | mode: 'cors', 22 | headers: Object.assign({ 23 | 'Accept': 'application/json', 24 | 'Content-Type': 'application/json', 25 | }, httpConfig.headers), 26 | body: JSON.stringify(data), 27 | }); 28 | } 29 | 30 | function createFlux({ port }) { 31 | const httpConfig = { 32 | host: `localhost:${port}`, 33 | protocol: 'http', 34 | }; 35 | return Flux.create({ 36 | actions: [ 37 | action('/users/create', async function createUser(flux, query, { userName, rank }) { 38 | await request(httpConfig, 'POST', `/users/create`, { userName, rank }); 39 | await flux.store('/users').fetch({}); 40 | }), 41 | action('/users/:userId/delete', async function deleteUser(flux, { userId }) { 42 | await request(httpConfig, 'DELETE', `/users/${userId}/delete`); 43 | await flux.store('/users').fetch({}); 44 | }), 45 | action('/users/:userId/update', async function updateUser(flux, query, { userName, rank }) { 46 | await request(httpConfig, 'POST', `/users/${query.userId}/update`, { userName, rank }); 47 | await flux.store('/users').fetch({}); 48 | }), 49 | action('/ui/users/toggle/visibility', async function toggleVisibilityUsers(flux) { 50 | const previousValue = await flux.store('/ui/users/visibility').fetch({}); 51 | flux.store('/ui/users/visibility').set({}, !previousValue.value); 52 | }), 53 | ], 54 | stores: [ 55 | storeHTTP('/users', httpConfig), 56 | storeMemory('/ui/users/visibility').set({}, true), 57 | ], 58 | }); 59 | } 60 | 61 | export default createFlux; 62 | -------------------------------------------------------------------------------- /config/gulp/tasks/build.js: -------------------------------------------------------------------------------- 1 | import babel from 'gulp-babel'; 2 | import changed from 'gulp-changed'; 3 | import gulp from 'gulp'; 4 | import path from 'path'; 5 | import plumber from 'gulp-plumber'; 6 | import sourcemaps from 'gulp-sourcemaps'; 7 | import gutil from 'gulp-util'; 8 | 9 | import babelConfig from '../../babel'; 10 | 11 | const root = path.join( 12 | __dirname, // /config/gulp/tasks 13 | '..', // /config/gulp 14 | '..', // /config/ 15 | '..', // / 16 | ); 17 | 18 | const lib = path.join(root, 'lib'); 19 | 20 | const sources = [ 21 | path.join(lib, '**', '*.jsx'), 22 | path.join(lib, '**', '*.js'), 23 | ]; 24 | 25 | const misc = sources.map((source) => `!${source}`).concat(path.join(lib, '**', '*')); 26 | 27 | const dist = path.join(root, 'dist'); 28 | 29 | function createCopy(platform, env) { 30 | return () => gulp.src(misc) 31 | .pipe(changed(path.join(dist, platform, env, 'lib'), { hasChanged: changed.compareSha1Digest })) 32 | .pipe(gulp.dest(path.join(dist, platform, env, 'lib'))); 33 | } 34 | 35 | function createBuild(platform, env) { 36 | return () => gulp.src(sources) 37 | .pipe(plumber({ 38 | errorHandler: (err) => console.error(err.stack), 39 | })) 40 | .pipe(env === 'prod' ? gutil.noop() : sourcemaps.init({ loadMaps: true })) 41 | .pipe(changed(path.join(dist, platform, env, 'lib'), { extension: '.js', hasChanged: changed.compareSha1Digest })) 42 | .pipe(babel(Object.assign({}, babelConfig[platform][env]))) 43 | .pipe(env === 'prod' ? gutil.noop() : sourcemaps.write('.')) 44 | .pipe(gulp.dest(path.join(dist, platform, env, 'lib'))) 45 | ; 46 | } 47 | 48 | export default () => { 49 | gulp.task('build', Object.keys(babelConfig).map((platform) => { 50 | const buildPlatformTaskName = `build-${platform}`; 51 | gulp.task(buildPlatformTaskName, Object.keys(babelConfig[platform]).map((env) => { 52 | const buildEnvTaskName = `build-${platform}-${env}`; 53 | const copyEnvTaskName = `copy-${platform}-${env}`; 54 | gulp.task(copyEnvTaskName, [`clean-${platform}-${env}`], createCopy(platform, env)); 55 | gulp.task(buildEnvTaskName, [copyEnvTaskName, 'lint'], createBuild(platform, env)); 56 | return buildEnvTaskName; 57 | })); 58 | return buildPlatformTaskName; 59 | })); 60 | }; 61 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/components/User.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import { deps } from '../../../'; 5 | 6 | export default deps(({ userId }) => ({ 7 | actions: { 8 | deleteUser: `/users/${userId}/delete`, 9 | updateUser: `/users/${userId}/update`, 10 | }, 11 | }))(class User extends React.Component { 12 | static displayName = 'User'; 13 | 14 | static propTypes = { 15 | deleteUser: PropTypes.func, 16 | rank: PropTypes.string, 17 | updateUser: PropTypes.func, 18 | userId: PropTypes.number, 19 | userName: PropTypes.string, 20 | }; 21 | 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | inputUserName: '', 26 | inputRank: '', 27 | }; 28 | } 29 | 30 | updateInputUserName(ev) { 31 | const inputUserName = ev.target.value; 32 | this.setState({ inputUserName }); 33 | } 34 | 35 | updateInputRank(ev) { 36 | const inputRank = ev.target.value; 37 | this.setState({ inputRank }); 38 | } 39 | 40 | updateUser({ userName, rank }) { 41 | const { updateUser } = this.props; 42 | updateUser({ userName, rank }); 43 | this.setState({ 44 | inputUserName: '', 45 | inputRank: '', 46 | }); 47 | } 48 | 49 | render() { 50 | const { userId, userName, rank } = this.props; 51 | const { inputUserName, inputRank } = this.state; 52 | return
53 |
{`User #${userId}`}
54 |
{`User Name: ${userName}`}
55 |
{`User Rank: ${rank}`}
56 | 57 |
58 | this.updateInputUserName(ev)} 61 | placeholder={'Updated Name'} 62 | value={inputUserName} 63 | /> 64 | this.updateInputRank(ev)} 67 | placeholder={'Updated Rank'} 68 | value={inputRank} 69 | /> 70 | 74 |
75 |
; 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /config/babel/browser/dev/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'babel-plugin-syntax-async-functions', 4 | 'babel-plugin-syntax-async-generators', 5 | 'babel-plugin-syntax-class-constructor-call', 6 | 'babel-plugin-syntax-class-properties', 7 | 'babel-plugin-syntax-decorators', 8 | 'babel-plugin-syntax-flow', 9 | 'babel-plugin-syntax-jsx', 10 | 'babel-plugin-syntax-object-rest-spread', 11 | 'babel-plugin-syntax-trailing-function-commas', 12 | ['babel-plugin-transform-async-to-module-method', { 13 | 'module': 'bluebird', 14 | 'method': 'coroutine', 15 | }], 16 | 'babel-plugin-transform-exponentiation-operator', 17 | 'babel-plugin-transform-object-rest-spread', 18 | 'babel-plugin-transform-flow-strip-types', 19 | 'babel-plugin-transform-react-display-name', 20 | 'babel-plugin-transform-react-jsx', 21 | 'babel-plugin-transform-eval', 22 | 'babel-plugin-transform-jscript', 23 | 'babel-plugin-transform-object-assign', 24 | 'babel-plugin-transform-object-set-prototype-of-to-assign', 25 | 'babel-plugin-transform-proto-to-assign', 26 | 'babel-plugin-transform-regenerator', 27 | 'babel-plugin-transform-runtime', 28 | 'babel-plugin-transform-undefined-to-void', 29 | 'babel-plugin-transform-class-constructor-call', 30 | 'babel-plugin-transform-class-properties', 31 | 'babel-plugin-transform-decorators-legacy', 32 | 'babel-plugin-transform-es2015-arrow-functions', 33 | 'babel-plugin-transform-es2015-block-scoped-functions', 34 | 'babel-plugin-transform-es2015-block-scoping', 35 | 'babel-plugin-transform-es2015-classes', 36 | 'babel-plugin-transform-es2015-computed-properties', 37 | 'babel-plugin-transform-es2015-constants', 38 | 'babel-plugin-transform-es2015-destructuring', 39 | 'babel-plugin-transform-es2015-for-of', 40 | 'babel-plugin-transform-es2015-function-name', 41 | 'babel-plugin-transform-es2015-literals', 42 | 'babel-plugin-transform-es2015-object-super', 43 | 'babel-plugin-transform-es2015-parameters', 44 | 'babel-plugin-transform-es2015-shorthand-properties', 45 | 'babel-plugin-transform-es2015-spread', 46 | 'babel-plugin-transform-es2015-sticky-regex', 47 | 'babel-plugin-transform-es2015-template-literals', 48 | 'babel-plugin-transform-es2015-typeof-symbol', 49 | 'babel-plugin-transform-es2015-unicode-regex', 50 | 'babel-plugin-transform-es2015-modules-commonjs', 51 | 'babel-plugin-lodash', 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /config/babel/node/dev/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'babel-plugin-syntax-async-functions', 4 | 'babel-plugin-syntax-async-generators', 5 | 'babel-plugin-syntax-class-constructor-call', 6 | 'babel-plugin-syntax-class-properties', 7 | 'babel-plugin-syntax-decorators', 8 | 'babel-plugin-syntax-flow', 9 | 'babel-plugin-syntax-jsx', 10 | 'babel-plugin-syntax-object-rest-spread', 11 | 'babel-plugin-syntax-trailing-function-commas', 12 | ['babel-plugin-transform-async-to-module-method', { 13 | 'module': 'bluebird', 14 | 'method': 'coroutine', 15 | }], 16 | 'babel-plugin-transform-class-constructor-call', 17 | 'babel-plugin-transform-class-properties', 18 | 'babel-plugin-transform-decorators-legacy', 19 | 'babel-plugin-transform-exponentiation-operator', 20 | 'babel-plugin-transform-object-rest-spread', 21 | 'babel-plugin-transform-flow-strip-types', 22 | 'babel-plugin-transform-react-display-name', 23 | 'babel-plugin-transform-react-jsx', 24 | 'babel-plugin-transform-eval', 25 | // 'babel-plugin-transform-jscript', 26 | 'babel-plugin-transform-object-assign', 27 | 'babel-plugin-transform-object-set-prototype-of-to-assign', 28 | 'babel-plugin-transform-proto-to-assign', 29 | // 'babel-plugin-transform-regenerator', 30 | 'babel-plugin-transform-runtime', 31 | 'babel-plugin-transform-undefined-to-void', 32 | // 'babel-plugin-transform-es2015-arrow-functions', 33 | // 'babel-plugin-transform-es2015-block-scoped-functions', 34 | // 'babel-plugin-transform-es2015-block-scoping', 35 | 'babel-plugin-transform-es2015-classes', 36 | // 'babel-plugin-transform-es2015-computed-properties', 37 | // 'babel-plugin-transform-es2015-constants', 38 | 'babel-plugin-transform-es2015-destructuring', 39 | // 'babel-plugin-transform-es2015-for-of', 40 | 'babel-plugin-transform-es2015-function-name', 41 | 'babel-plugin-transform-es2015-literals', 42 | 'babel-plugin-transform-es2015-object-super', 43 | 'babel-plugin-transform-es2015-parameters', 44 | // 'babel-plugin-transform-es2015-shorthand-properties', 45 | 'babel-plugin-transform-es2015-spread', 46 | 'babel-plugin-transform-es2015-sticky-regex', 47 | // 'babel-plugin-transform-es2015-template-literals', 48 | 'babel-plugin-transform-es2015-typeof-symbol', 49 | 'babel-plugin-transform-es2015-unicode-regex', 50 | 'babel-plugin-transform-es2015-modules-commonjs', 51 | ], 52 | sourceMaps: 'both', 53 | retainLines: true, 54 | }; 55 | -------------------------------------------------------------------------------- /lib/__tests__/browser/deleteUser.js: -------------------------------------------------------------------------------- 1 | import cheerio from 'cheerio'; 2 | import React from 'react'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import should from 'should/as-function'; 5 | 6 | import startServersAndBrowse from '../fixtures/startServersAndBrowse'; 7 | 8 | const { load } = cheerio; 9 | const TIME_OUT = 250; 10 | 11 | function removeReactAttributes(html) { 12 | return html.replace(/ (data-reactid|data-react-checksum|data-reactroot)=".*?"/g, ''); 13 | } 14 | 15 | const { after, before, describe, it, browser } = global; 16 | describe('[FT] Delete User', () => { 17 | 18 | let stopServers = null; 19 | 20 | before(async function $before(done) { 21 | stopServers = await startServersAndBrowse(browser); 22 | done(); 23 | }); 24 | after(async function $after() { 25 | return await stopServers(); 26 | }); 27 | 28 | it('should dispatch deleteUser action and check if a user has been deleted', () => { 29 | const expectedAppHtml = load(ReactDOMServer.renderToStaticMarkup( 30 |
31 |
32 |
33 |
34 | 37 |
38 |
    39 |
  • 40 |
    41 |
    {'User #1'}
    42 |
    {'User Name: Martin'}
    43 |
    {'User Rank: Gold'}
    44 | 45 |
    46 | 47 | 48 | 49 |
    50 |
    51 |
  • 52 |
53 | 54 | 55 | 56 |
57 |
58 |
)).html(); 59 | return browser 60 | .click('.Users li:last-child > div > button') 61 | .pause(TIME_OUT) 62 | .getHTML('#__App__', (err, appHtml) => { 63 | if(err) { 64 | return err; 65 | } 66 | should(load(removeReactAttributes(appHtml)).html()).be.equal(expectedAppHtml); 67 | }); 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /lib/__tests__/node/stores.js: -------------------------------------------------------------------------------- 1 | const { describe, it, beforeEach, afterEach } = global; 2 | import should from 'should/as-function'; 3 | import sinon from 'sinon'; 4 | 5 | import ApiServer from '../fixtures/ApiServer'; 6 | 7 | import { HTTPStore, MemoryStore } from '../../'; 8 | 9 | const API_PORT = 7654; 10 | 11 | describe('HTTP.Store', () => { 12 | describe('#options', () => { 13 | describe('rewritePath', () => { 14 | let apiServer = null; 15 | 16 | beforeEach(() => { 17 | apiServer = new ApiServer({ port: API_PORT }); 18 | return apiServer.startListening(); 19 | }); 20 | 21 | afterEach(() => apiServer.stopListening()); 22 | 23 | it('should correctly access the server with the rewritten path for any path of the store', async function test() { 24 | const store = HTTPStore.create( 25 | '/test/users/:userId/custom/path', 26 | { protocol: 'http', hostname: 'localhost', port: API_PORT }, 27 | { 28 | rewritePath({ userId }) { 29 | return `/users/${userId}`; 30 | }, 31 | } 32 | ); 33 | 34 | const user1Info = await store.fetch({ userId: '1' }); 35 | should(user1Info.isResolved()).be.true(); 36 | should(user1Info.value).be.deepEqual({ userId: 1, userName: 'Martin', rank: 'Gold' }); 37 | 38 | const user2Info = await store.fetch({ userId: '2' }); 39 | should(user2Info.isResolved()).be.true(); 40 | should(user2Info.value).be.deepEqual({ userId: 2, userName: 'Matthieu', rank: 'Silver' }); 41 | }); 42 | 43 | it('should correctly access the server with the rewritten path with a query', async function test() { 44 | const store = HTTPStore.create( 45 | '/foo/:foo/bar/:bar', 46 | { protocol: 'http', hostname: 'localhost', port: API_PORT }, 47 | { 48 | rewritePath({ foo, bar }) { 49 | return `/echoQuery?foo=${foo}&bar=${bar}`; 50 | }, 51 | } 52 | ); 53 | 54 | const query = { foo: 'baz', bar: 'qux' }; 55 | const echo = await store.fetch(query); 56 | should(echo.isResolved()).be.true(); 57 | should(echo.value).be.deepEqual(query); 58 | }); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('Memory.Store', () => { 64 | describe('#observe', () => { 65 | const onChange = sinon.stub().returns(); 66 | it('should initializes and observe memory store correctly', () => { 67 | const store = MemoryStore.create('/foo/:bar'); 68 | store.observe({ bar: 'bar' }, onChange); 69 | store.set({ bar: 'bar' }, 'foo'); 70 | 71 | sinon.assert.called(onChange); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /config/babel/node/prod/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'babel-plugin-syntax-async-functions', 4 | 'babel-plugin-syntax-async-generators', 5 | 'babel-plugin-syntax-class-constructor-call', 6 | 'babel-plugin-syntax-class-properties', 7 | 'babel-plugin-syntax-decorators', 8 | 'babel-plugin-syntax-flow', 9 | 'babel-plugin-syntax-jsx', 10 | 'babel-plugin-syntax-object-rest-spread', 11 | 'babel-plugin-syntax-trailing-function-commas', 12 | 'babel-plugin-transform-class-constructor-call', 13 | 'babel-plugin-transform-class-properties', 14 | 'babel-plugin-transform-decorators-legacy', 15 | 'babel-plugin-transform-exponentiation-operator', 16 | 'babel-plugin-transform-object-rest-spread', 17 | 'babel-plugin-transform-flow-strip-types', 18 | 'babel-plugin-transform-react-constant-elements', 19 | 'babel-plugin-transform-react-display-name', 20 | 'babel-plugin-transform-react-inline-elements', 21 | 'babel-plugin-transform-react-jsx', 22 | 'babel-plugin-transform-eval', 23 | // 'babel-plugin-transform-jscript', 24 | 'babel-plugin-transform-object-assign', 25 | 'babel-plugin-transform-object-set-prototype-of-to-assign', 26 | 'babel-plugin-transform-proto-to-assign', 27 | // 'babel-plugin-transform-regenerator', 28 | 'babel-plugin-transform-runtime', 29 | 'babel-plugin-transform-undefined-to-void', 30 | // 'babel-plugin-transform-es2015-arrow-functions', 31 | // 'babel-plugin-transform-es2015-block-scoped-functions', 32 | // 'babel-plugin-transform-es2015-block-scoping', 33 | ['babel-plugin-transform-es2015-classes', { loose: true }], 34 | ['babel-plugin-transform-es2015-computed-properties', { loose: true }], 35 | // 'babel-plugin-transform-es2015-constants', 36 | ['babel-plugin-transform-es2015-destructuring', { loose: true }], 37 | // ['babel-plugin-transform-es2015-for-of', { loose: true }], 38 | 'babel-plugin-transform-es2015-function-name', 39 | 'babel-plugin-transform-es2015-literals', 40 | 'babel-plugin-transform-es2015-object-super', 41 | 'babel-plugin-transform-es2015-parameters', 42 | // 'babel-plugin-transform-es2015-shorthand-properties', 43 | 'babel-plugin-transform-es2015-spread', 44 | 'babel-plugin-transform-es2015-sticky-regex', 45 | // ['babel-plugin-transform-es2015-template-literals', { loose: true }], 46 | 'babel-plugin-transform-es2015-typeof-symbol', 47 | 'babel-plugin-transform-es2015-unicode-regex', 48 | ['babel-plugin-transform-es2015-modules-commonjs', { loose: true }], 49 | ['babel-plugin-transform-async-to-module-method', { 50 | 'module': 'bluebird', 51 | 'method': 'coroutine', 52 | }], 53 | ], 54 | }; 55 | -------------------------------------------------------------------------------- /config/babel/browser/prod/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'babel-plugin-syntax-async-functions', 4 | 'babel-plugin-syntax-async-generators', 5 | 'babel-plugin-syntax-class-constructor-call', 6 | 'babel-plugin-syntax-class-properties', 7 | 'babel-plugin-syntax-decorators', 8 | 'babel-plugin-syntax-flow', 9 | 'babel-plugin-syntax-jsx', 10 | 'babel-plugin-syntax-object-rest-spread', 11 | 'babel-plugin-syntax-trailing-function-commas', 12 | ['babel-plugin-transform-async-to-module-method', { 13 | 'module': 'bluebird', 14 | 'method': 'coroutine', 15 | }], 16 | 'babel-plugin-transform-class-constructor-call', 17 | 'babel-plugin-transform-class-properties', 18 | 'babel-plugin-transform-decorators-legacy', 19 | 'babel-plugin-transform-exponentiation-operator', 20 | 'babel-plugin-transform-object-rest-spread', 21 | 'babel-plugin-transform-flow-strip-types', 22 | // 'babel-plugin-transform-react-constant-elements', 23 | 'babel-plugin-transform-react-display-name', 24 | 'babel-plugin-transform-react-inline-elements', 25 | 'babel-plugin-transform-react-jsx', 26 | 'babel-plugin-transform-eval', 27 | 'babel-plugin-transform-jscript', 28 | 'babel-plugin-transform-object-assign', 29 | 'babel-plugin-transform-object-set-prototype-of-to-assign', 30 | 'babel-plugin-transform-proto-to-assign', 31 | 'babel-plugin-transform-regenerator', 32 | 'babel-plugin-transform-runtime', 33 | 'babel-plugin-transform-undefined-to-void', 34 | 'babel-plugin-transform-es2015-arrow-functions', 35 | 'babel-plugin-transform-es2015-block-scoped-functions', 36 | 'babel-plugin-transform-es2015-block-scoping', 37 | ['babel-plugin-transform-es2015-classes', { loose: true }], 38 | ['babel-plugin-transform-es2015-computed-properties', { loose: true }], 39 | 'babel-plugin-transform-es2015-constants', 40 | ['babel-plugin-transform-es2015-destructuring', { loose: true }], 41 | ['babel-plugin-transform-es2015-for-of', { loose: true }], 42 | 'babel-plugin-transform-es2015-function-name', 43 | 'babel-plugin-transform-es2015-literals', 44 | 'babel-plugin-transform-es2015-object-super', 45 | 'babel-plugin-transform-es2015-parameters', 46 | 'babel-plugin-transform-es2015-shorthand-properties', 47 | 'babel-plugin-transform-es2015-spread', 48 | 'babel-plugin-transform-es2015-sticky-regex', 49 | ['babel-plugin-transform-es2015-template-literals', { loose: true }], 50 | 'babel-plugin-transform-es2015-typeof-symbol', 51 | 'babel-plugin-transform-es2015-unicode-regex', 52 | ['babel-plugin-transform-es2015-modules-commonjs', { loose: true }], 53 | 'babel-plugin-lodash', 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/RenderServer.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import createApp from 'koa'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import should from 'should/as-function'; 5 | import React from 'react'; 6 | import { prepare } from '../../'; 7 | 8 | import App from './components/App'; 9 | import createFlux from './createFlux'; 10 | import pageTemplate from './pageTemplate'; 11 | 12 | const __DEV__ = process.env.NODE_ENV === 'development'; 13 | 14 | // These symbols enumerate the possible states of a Server instance 15 | const [NOT_STARTED, STARTED, STOPPED] = [Symbol('NOT_STARTED'), Symbol('STARTED'), Symbol('STOPPED')]; 16 | 17 | class RenderServer { 18 | /** 19 | * koa app 20 | * @type {Object} 21 | */ 22 | app = null; 23 | 24 | /** 25 | * server status 26 | * @type {Symbol} 27 | */ 28 | _status = null; 29 | 30 | /** 31 | * server instance 32 | * @type {Object} 33 | */ 34 | server = null; 35 | 36 | constructor(config) { 37 | this._status = NOT_STARTED; 38 | this.app = createApp(); 39 | this.config = config; 40 | const { apiPort } = config; 41 | 42 | this.app.use(function* render() { 43 | const flux = createFlux({ port: apiPort }); 44 | const app = ; 45 | yield prepare(app); 46 | this.body = pageTemplate({ 47 | title: 'UserList', 48 | appHtml: ReactDOMServer.renderToString(app), 49 | nexusPayload: JSON.stringify(flux.dumpState()), 50 | httpConfig: JSON.stringify({ port: apiPort }), 51 | }); 52 | }); 53 | } 54 | 55 | /** 56 | * Start listening for incoming requests 57 | * @return {Promise} Resolves when/if the server successfully starts 58 | */ 59 | startListening() { 60 | const { port } = this.config; 61 | return new Promise((resolve, reject) => { 62 | if(__DEV__) { 63 | should(this._status).be.exactly(NOT_STARTED); 64 | } 65 | 66 | // Grab a reference to the HTTPServer instance so we can close it later 67 | this.server = this.app.listen(port, (err) => { 68 | if(err) { 69 | return reject(err); 70 | } 71 | return resolve(); 72 | }); 73 | }) 74 | .then(() => { 75 | this._status = STARTED; 76 | }); 77 | } 78 | 79 | /** 80 | * Stop listening for incoming requests 81 | * @return {Promise} Resolves when/if the server successfully stops 82 | */ 83 | stopListening() { 84 | return Promise.try(() => { 85 | if(__DEV__) { 86 | should(this._status).be.exactly(STARTED); 87 | } 88 | return Promise.resolve(this.server.close()); 89 | }) 90 | .then(() => 91 | this._status = STOPPED 92 | ); 93 | } 94 | } 95 | 96 | export default RenderServer; 97 | -------------------------------------------------------------------------------- /config/gulp/tasks/selenium.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import gulp from 'gulp'; 4 | import plumber from 'gulp-plumber'; 5 | import rename from 'gulp-rename'; 6 | import sourcemaps from 'gulp-sourcemaps'; 7 | import gutil from 'gulp-util'; 8 | import uglify from 'gulp-uglify'; 9 | import webdriver from 'gulp-webdriver'; 10 | import rimraf from 'rimraf'; 11 | import selenium from 'selenium-standalone'; 12 | import webpack from 'webpack'; 13 | import gwebpack from 'webpack-stream'; 14 | 15 | import babelConfig from '../../babel'; 16 | import webpackConfig from '../../../webpackConfig'; 17 | 18 | const root = path.join( 19 | __dirname, // /config/gulp/tasks 20 | '..', // /config/gulp 21 | '..', // /config/ 22 | '..', // / 23 | ); 24 | 25 | const dist = path.join(root, 'dist'); 26 | const lib = path.join(root, 'lib'); 27 | const wdioConfig = path.join(root, 'config', 'wdio'); 28 | 29 | const staticAssets = { 30 | js: path.join('static', 'c.js'), 31 | }; 32 | 33 | let seleniumServer; 34 | 35 | // Setting build for production & development environments 36 | Object.keys(babelConfig.browser).map((env) => { 37 | const isProd = env === 'prod'; 38 | 39 | // Build the bundle of the client script with webpack. 40 | return gulp.task(`build-selenium-${env}`, [`build-browser-${env}`], () => 41 | gulp.src(path.join(lib, '__tests__', 'fixtures', 'RenderClient.js')) 42 | .pipe(plumber()) 43 | .pipe(isProd ? gutil.noop() : sourcemaps.init()) 44 | .pipe(gwebpack(webpackConfig(env), webpack)) 45 | .pipe(rename(staticAssets.js)) 46 | .pipe(isProd ? uglify({ 47 | mangle: { except: ['GeneratorFunction'] }, 48 | }) : gutil.noop()) 49 | .pipe(isProd ? gutil.noop() : sourcemaps.write()) 50 | .pipe(gulp.dest(path.join(dist, 'browser', env))) 51 | ); 52 | }); 53 | 54 | // Install & Start selenium server. 55 | gulp.task('selenium', (done) => { 56 | 57 | // Install selenium server with default config: (chrome & ie drivers). 58 | selenium.install({}, (errorInstall) => { 59 | if(errorInstall) { 60 | return done(errorInstall); 61 | } 62 | 63 | // Start selenium server. 64 | selenium.start((errStart, child) => { 65 | if (errStart) { 66 | return done(errStart); 67 | } 68 | seleniumServer = child; 69 | done(); 70 | }); 71 | }); 72 | }); 73 | 74 | export default () => { 75 | 76 | // Setting tests for production & development environments 77 | Object.keys(babelConfig.browser).map((env) => { 78 | const testSeleniumName = `test-selenium-${env}`; 79 | 80 | // Start tests with the selenium setup. 81 | gulp.task(testSeleniumName, ['selenium', `build-selenium-${env}`], () => 82 | gulp.src(path.join(wdioConfig, env, 'wdio.conf.js')) 83 | .pipe(webdriver()) 84 | .once('end', () => { 85 | rimraf(path.join(dist, 'browser', env, 'static'), (err) => { 86 | if(err) { 87 | console.log(err); 88 | } 89 | }); 90 | seleniumServer.kill(); 91 | }) 92 | ); 93 | return testSeleniumName; 94 | }); 95 | }; 96 | -------------------------------------------------------------------------------- /lib/actions.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import _ from 'lodash'; 4 | const __DEV__ = process && process.env && process.env.NODE_ENV === 'development'; 5 | 6 | import Flux from './Flux'; 7 | import defaultFluxKey from './defaultFluxKey'; 8 | import shouldPureComponentUpdate from './util/shouldPureComponentUpdate'; 9 | 10 | /** 11 | * Enhance a React Component and make context's {@link Flux}'s {@link Actions}s requested by bindings avaliable as props. 12 | * @param {Function} getBindings Function given the component own props, returns Actions bindings. 13 | * @param {Object} options Options object. 14 | * @param {String} [options.displayName] The displayName of the wrapper component (useful for debugging). 15 | * @param {String} [options.fluxKey] Use a specific string as fluxKey rather than the default one. 16 | * @param {Function} [options.shouldNexusComponentUpdate] Use specific function for shouldComponentUpdate event. 17 | * @return {Component} Enhanced component. 18 | */ 19 | function actions(getBindings, { 20 | displayName = void 0, 21 | fluxKey = defaultFluxKey, 22 | shouldNexusComponentUpdate = shouldPureComponentUpdate, 23 | } = {}) { 24 | return (Component) => { 25 | /** 26 | * Represents a Component wrapper used to make binded {@link Actions} avaliable as props. 27 | */ 28 | class ActionsComponent extends React.Component { 29 | static displayName = displayName || `@actions(${Component.displayName})`; 30 | 31 | static contextTypes = { 32 | [fluxKey]: PropTypes.instanceOf(Flux), 33 | }; 34 | 35 | /** 36 | * React lifecycle method called when the component will receive updated props or state. 37 | * @param {Object} args Arguments provided when shouldComponentUpdate is triggered: nextProps, nextState. 38 | * @return {undefined} 39 | */ 40 | shouldComponentUpdate(...args) { 41 | return Reflect.apply(shouldNexusComponentUpdate, this, args); 42 | } 43 | 44 | /** 45 | * Renders the ActionsComponent. 46 | * @return {ReactElement|false|null} Renderded component. 47 | */ 48 | render() { 49 | const { props, context } = this; 50 | const flux = context[fluxKey]; 51 | const bindings = getBindings(props, flux); 52 | const dispatchProps = _.mapValues(bindings, (path) => async function dispatchAction(...params) { 53 | return await flux.dispatchAction(path, ...params); 54 | }); 55 | if(__DEV__) { 56 | const inter = _.intersection(Object.keys(dispatchProps), Object.keys(props)); 57 | if(inter.length > 0) { 58 | console.warn( 59 | 'Warning: conflicting keys between props and dispatchProps in ActionsComponent', 60 | this.constructor.displayName, 61 | inter, 62 | ); 63 | } 64 | } 65 | const childProps = Object.assign({}, dispatchProps, props); 66 | return ; 67 | } 68 | } 69 | 70 | return ActionsComponent; 71 | }; 72 | } 73 | 74 | export default actions; 75 | -------------------------------------------------------------------------------- /lib/__tests__/browser/updateUser.js: -------------------------------------------------------------------------------- 1 | import cheerio from 'cheerio'; 2 | import React from 'react'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import should from 'should/as-function'; 5 | 6 | import startServersAndBrowse from '../fixtures/startServersAndBrowse'; 7 | 8 | const { load } = cheerio; 9 | const TIME_OUT = 250; 10 | 11 | function removeReactAttributes(html) { 12 | return html.replace(/ (data-reactid|data-react-checksum|data-reactroot)=".*?"/g, ''); 13 | } 14 | 15 | const { after, before, describe, it, browser } = global; 16 | describe('[FT] Update User', () => { 17 | 18 | let stopServers = null; 19 | 20 | before(async function $before(done) { 21 | stopServers = await startServersAndBrowse(browser); 22 | done(); 23 | }); 24 | after(async function $after() { 25 | return await stopServers(); 26 | }); 27 | 28 | it('should dispatch updateUser action and check if a user has been updated', () => { 29 | const expectedAppHtml = load(ReactDOMServer.renderToStaticMarkup( 30 |
31 |
32 |
33 |
34 | 37 |
38 |
    39 |
  • 40 |
    41 |
    {'User #1'}
    42 |
    {'User Name: Plante'}
    43 |
    {'User Rank: Challenger'}
    44 | 45 |
    46 | 47 | 48 | 49 |
    50 |
    51 |
  • 52 |
  • 53 |
    54 |
    {'User #2'}
    55 |
    {'User Name: Matthieu'}
    56 |
    {'User Rank: Silver'}
    57 | 58 |
    59 | 60 | 61 | 62 |
    63 |
    64 |
  • 65 |
66 | 67 | 68 | 69 |
70 |
71 |
)).html(); 72 | return browser 73 | .waitForExist('#CreateUser') 74 | .setValue('#InputUserName-1', 'Plante') 75 | .setValue('#InputUserRank-1', 'Challenger') 76 | .click('.Users li:first-child > div > div > button') 77 | .pause(TIME_OUT) 78 | .getHTML('#__App__', (err, appHtml) => { 79 | if(err) { 80 | return err; 81 | } 82 | should(load(removeReactAttributes(appHtml)).html()).be.equal(expectedAppHtml); 83 | }); 84 | }); 85 | 86 | }); 87 | -------------------------------------------------------------------------------- /lib/__tests__/fixtures/components/Users.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import { deps, Store } from '../../../'; 5 | 6 | import User from './User'; 7 | 8 | export default deps(() => ({ 9 | actions: { 10 | createUser: `/users/create`, 11 | toggleUsersVisibility: `/ui/users/toggle/visibility`, 12 | }, 13 | stores: { 14 | users: '/users', 15 | uiUsersVisibility: '/ui/users/visibility', 16 | }, 17 | }))(class Users extends React.Component { 18 | static displayName = 'Users'; 19 | 20 | static propTypes = { 21 | createUser: PropTypes.func, 22 | optionalStore: Store.State.propType(React.any), 23 | toggleUsersVisibility: PropTypes.func, 24 | uiUsersVisibility: Store.State.propType(PropTypes.bool.isRequired).isRequired, 25 | users: Store.State.propType(PropTypes.arrayOf(PropTypes.shape({ 26 | userId: PropTypes.number.isRequired, 27 | userName: PropTypes.string.isRequired, 28 | rank: PropTypes.string.isRequired, 29 | }))).isRequired, 30 | }; 31 | 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | inputUserName: '', 36 | inputRank: '', 37 | }; 38 | } 39 | 40 | updateInputUserName(ev) { 41 | const inputUserName = ev.target.value; 42 | this.setState({ inputUserName }); 43 | } 44 | 45 | updateInputRank(ev) { 46 | const inputRank = ev.target.value; 47 | this.setState({ inputRank }); 48 | } 49 | 50 | createUser({ userName, rank }) { 51 | const { createUser } = this.props; 52 | createUser({ userName, rank }); 53 | this.setState({ 54 | inputUserName: '', 55 | inputRank: '', 56 | }); 57 | } 58 | 59 | render() { 60 | const { users, uiUsersVisibility } = this.props; 61 | const { inputUserName, inputRank } = this.state; 62 | if(users.isPending()) { 63 | return
64 | {'Loading...'} 65 |
; 66 | } 67 | if(users.isRejected()) { 68 | return
69 | {'Error: '}{users.reason} 70 |
; 71 | } 72 | const userList = users.value; 73 | const userListVisibility = uiUsersVisibility.value; 74 | return
75 |
76 | 81 |
82 | { userListVisibility ?
    {userList.map(({ userName, rank, userId }) => 83 |
  • 84 | 85 |
  • 86 | )}
: null } 87 | this.updateInputUserName(ev)} 90 | placeholder={'User Name'} 91 | value={inputUserName} 92 | /> 93 | this.updateInputRank(ev)} 96 | placeholder={'Rank'} 97 | value={inputRank} 98 | /> 99 | 107 |
; 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /lib/__tests__/browser/createUser.js: -------------------------------------------------------------------------------- 1 | import cheerio from 'cheerio'; 2 | import React from 'react'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import should from 'should/as-function'; 5 | 6 | import startServersAndBrowse from '../fixtures/startServersAndBrowse'; 7 | 8 | const { load } = cheerio; 9 | const TIME_OUT = 250; 10 | 11 | function removeReactAttributes(html) { 12 | return html.replace(/ (data-reactid|data-react-checksum|data-reactroot)=".*?"/g, ''); 13 | } 14 | 15 | const { after, before, describe, it, browser } = global; 16 | describe('[FT] Create User', () => { 17 | 18 | let stopServers = null; 19 | 20 | before(async function $before(done) { 21 | stopServers = await startServersAndBrowse(browser); 22 | done(); 23 | }); 24 | after(async function $after() { 25 | return await stopServers(); 26 | }); 27 | 28 | it('should dispatch createUser action and check if a new user has been added', () => { 29 | const expectedAppHtml = load(ReactDOMServer.renderToStaticMarkup( 30 |
31 |
32 |
33 |
34 | 37 |
38 |
    39 |
  • 40 |
    41 |
    {'User #1'}
    42 |
    {'User Name: Martin'}
    43 |
    {'User Rank: Gold'}
    44 | 45 |
    46 | 47 | 48 | 49 |
    50 |
    51 |
  • 52 |
  • 53 |
    54 |
    {'User #2'}
    55 |
    {'User Name: Matthieu'}
    56 |
    {'User Rank: Silver'}
    57 | 58 |
    59 | 60 | 61 | 62 |
    63 |
    64 |
  • 65 |
  • 66 |
    67 |
    {'User #3'}
    68 |
    {'User Name: Nicolas'}
    69 |
    {'User Rank: Diamond'}
    70 | 71 |
    72 | 73 | 74 | 75 |
    76 |
    77 |
  • 78 |
79 | 80 | 81 | 82 |
83 |
84 |
)).html(); 85 | return browser 86 | .waitForExist('#CreateUser') 87 | .setValue('#InputUserName', 'Nicolas') 88 | .setValue('#InputUserRank', 'Diamond') 89 | .click('#CreateUser') 90 | .pause(TIME_OUT) 91 | .getHTML('#__App__', (err, appHtml) => { 92 | if(err) { 93 | return err; 94 | } 95 | should(load(removeReactAttributes(appHtml)).html()).be.equal(expectedAppHtml); 96 | }); 97 | }); 98 | 99 | }); 100 | -------------------------------------------------------------------------------- /lib/__tests__/browser/renderedPage.js: -------------------------------------------------------------------------------- 1 | import cheerio from 'cheerio'; 2 | import React from 'react'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import should from 'should/as-function'; 5 | 6 | import startServersAndBrowse from '../fixtures/startServersAndBrowse'; 7 | 8 | const { load } = cheerio; 9 | 10 | function removeReactAttributes(html) { 11 | return html.replace(/ (data-reactid|data-react-checksum|data-reactroot)=".*?"/g, ''); 12 | } 13 | 14 | const { after, before, describe, it, browser } = global; 15 | describe('[FT] Rendered Page', () => { 16 | 17 | let stopServers = null; 18 | 19 | before(async function $before(done) { 20 | stopServers = await startServersAndBrowse(browser); 21 | done(); 22 | }); 23 | after(async function $after() { 24 | return await stopServers(); 25 | }); 26 | 27 | it('should title return the value correctly', () => 28 | browser 29 | .getTitle((err, title) => { 30 | if(err) { 31 | return err; 32 | } 33 | should(title).be.equal('UserList'); 34 | }) 35 | ); 36 | 37 | it('should __NEXUS_PAYLOAD__ property exists on the window object', () => 38 | browser 39 | .execute(() => window.__NEXUS_PAYLOAD__, (err, value) => { 40 | if(err) { 41 | return err; 42 | } 43 | return value; 44 | }).then(({ value: nexusPayload }) => { 45 | should.exist(nexusPayload); 46 | }) 47 | ); 48 | 49 | it('should