├── .babelrc
├── .cfignore
├── .env
├── .eslintrc
├── .gitignore
├── .react-tools
├── .travis.yml
├── LICENSE
├── Procfile
├── Procfile.dev
├── README.md
├── Staticfile
├── app
├── api
│ └── fake_posts_api.js
├── components
│ ├── api_page.js
│ ├── application.js
│ ├── layout.js
│ ├── router.js
│ ├── todo_adder.js
│ ├── todo_item.js
│ ├── todo_list.js
│ ├── todo_page.js
│ ├── use_router.js
│ ├── user_create_page.js
│ └── user_list_page.js
├── config.js
├── dispatchers
│ ├── api_dispatcher.js
│ └── main_dispatcher.js
├── index.js
├── index.jsx
├── store.js
└── stylesheets
│ ├── _layout.scss
│ ├── application.scss
│ └── postcss.config.js
├── config
├── application.json
├── development.json
├── env.json
├── integration.json
├── production.json
├── test.json
└── webpack
│ ├── development.js
│ ├── production.js
│ └── test.js
├── gulpfile.js
├── helpers
├── application_helper.js
└── fetch_helper.js
├── index.js
├── manifest.yml
├── package.json
├── server
├── app.js
├── bootstrap.js
└── env.js
├── spec
├── app
│ ├── api
│ │ └── fake_posts_api_spec.js
│ ├── components
│ │ ├── api_page_spec.js
│ │ ├── application_spec.js
│ │ ├── router_spec.js
│ │ ├── todo_adder_spec.js
│ │ ├── todo_item_spec.js
│ │ ├── todo_list_spec.js
│ │ ├── todo_page_spec.js
│ │ ├── use_router_spec.js
│ │ ├── user_create_page_spec.js
│ │ └── user_list_page_spec.js
│ ├── dispatchers
│ │ ├── api_dispatcher_spec.js
│ │ └── main_dispatcher_spec.js
│ ├── index.js
│ ├── spec_helper.js
│ └── support
│ │ ├── dispatcher_matchers.js
│ │ ├── mock_router.js
│ │ └── mock_router_spec.js
├── factories
│ └── user.js
├── integration
│ ├── features_spec.js
│ ├── helpers
│ │ └── webdriver_helper.js
│ ├── spec_helper.js
│ └── support
│ │ ├── jasmine_webdriver.js
│ │ └── selenium.js
├── spec_helper.js
└── support
│ ├── bluebird.js
│ ├── deferred.js
│ └── mock_fetch.js
├── tasks
├── default.js
├── deploy.js
├── dev_server.js
├── integration.js
├── react_tools.js
└── server.js
├── tmp
└── .gitkeep
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "development": {
4 | "plugins": [
5 | "react-hot-loader/babel"
6 | ]
7 | }
8 | },
9 | "presets": [["es2015", {"loose": true}], "react", "stage-0"],
10 | "plugins": [
11 | "add-module-exports",
12 | "transform-object-assign",
13 | "transform-react-display-name"
14 | ]
15 | }
--------------------------------------------------------------------------------
/.cfignore:
--------------------------------------------------------------------------------
1 | .env
2 | .idea
3 | /app/*
4 | /config/*
5 | /lib/*
6 | /logs/*
7 | /node_modules
8 | /scripts/*
9 | /server/*
10 | /spec/*
11 | /tasks/*
12 | /tmp/*
13 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | {
2 | "NODE_ENV": "development",
3 | "PORT": 3000,
4 | "API_PORT": 3001
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "browser": true,
5 | "phantomjs": true,
6 | "node": true,
7 | "jasmine": true
8 | },
9 |
10 | "ecmaFeatures": {
11 | "modules": true
12 | },
13 |
14 | "globals": {
15 | "MyReactStarter": true,
16 | "root": true,
17 | "Bluebird": true,
18 | "Dispatcher": true,
19 | "jQuery": true,
20 | "MockPromises": true,
21 | "MockRouter": true,
22 | "React": true,
23 | "ReactDOM": true,
24 | "setProps": true,
25 | "Factory": true,
26 | "$": true,
27 | "click": true,
28 | "describeWithWebdriver": true,
29 | "setValue": true,
30 | "sleep": true,
31 | "visit": true,
32 | "waitForExist": true,
33 | "waitForText": true
34 | },
35 |
36 | "parser": "babel-eslint",
37 |
38 | "plugins": [
39 | "react"
40 | ],
41 |
42 | "rules": {
43 | "block-scoped-var": 2,
44 | "camelcase": 0,
45 | "complexity": 2,
46 | "consistent-return": 0,
47 | "curly": 0,
48 | "default-case": 2,
49 | "dot-notation": 2,
50 | "eqeqeq": 2,
51 | "eol-last": 0,
52 | "guard-for-in": 2,
53 | "jsx-quotes": 2,
54 | "no-alert": 2,
55 | "no-caller": 2,
56 | "no-console": 2,
57 | "no-debugger": 2,
58 | "no-div-regex": 2,
59 | "no-else-return": 2,
60 | "no-eq-null": 2,
61 | "no-eval": 2,
62 | "no-extend-native": 2,
63 | "no-extra-bind": 2,
64 | "no-fallthrough": 2,
65 | "no-floating-decimal": 2,
66 | "no-implied-eval": 2,
67 | "no-iterator": 2,
68 | "no-labels": 2,
69 | "no-lone-blocks": 2,
70 | "no-loop-func": 2,
71 | "no-multi-spaces": 2,
72 | "no-multi-str": 2,
73 | "no-native-reassign": 2,
74 | "no-new": 2,
75 | "no-new-func": 2,
76 | "no-new-wrappers": 2,
77 | "no-octal": 2,
78 | "no-octal-escape": 2,
79 | "no-path-concat": 0,
80 | "no-process-env": 0,
81 | "no-proto": 2,
82 | "no-redeclare": 2,
83 | "no-return-assign": 0,
84 | "no-script-url": 2,
85 | "no-self-compare": 2,
86 | "no-sequences": 0,
87 | "no-shadow": 0,
88 | "no-undef": 2,
89 | "no-underscore-dangle": 0,
90 | "no-unused-expressions": 0,
91 | "no-unused-vars": 2,
92 | "no-var": 2,
93 | "no-void": 2,
94 | "no-warning-comments": 1,
95 | "no-with": 2,
96 | "quotes": [2, "single"],
97 | "radix": 2,
98 | "react/jsx-uses-vars": 2,
99 | "react/jsx-no-undef": 2,
100 | "react/jsx-uses-react": 2,
101 | "react/no-is-mounted": 2,
102 | "react/react-in-jsx-scope": 2,
103 | "react/prefer-es6-class": 1,
104 | "react/no-did-mount-set-state": 0,
105 | "react/no-did-update-set-state": 2,
106 | "react/prop-types": [2, {ignore: ['children', 'className', 'id', 'style']}],
107 | "react/self-closing-comp": 2,
108 | "react/sort-comp": 2,
109 | "react/jsx-wrap-multilines": 2,
110 | "semi": 2,
111 | "strict": 0,
112 | "vars-on-top": 0,
113 | "wrap-iife": [2, "inside"],
114 | "yoda": 0
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env.json
2 | .idea
3 | /config/local.json
4 | /dist/*
5 | /node_modules
6 | /public/*
7 | /logs/*
8 | /tmp/*
9 | .DS_Store
10 | npm-debug.log
11 |
--------------------------------------------------------------------------------
/.react-tools:
--------------------------------------------------------------------------------
1 | const base = require('pui-react-tools/webpack/base');
2 | const development = require('pui-react-tools/webpack/development');
3 |
4 | module.exports = {
5 | webpack: {
6 | base: {...base, entry: {application: './app/index.js'}},
7 | development: {
8 | entry: {
9 | application: ['react-hot-loader/patch', 'webpack-hot-middleware/client', './app/index.js']
10 | },
11 | plugins: development.plugins
12 | },
13 | integration: {
14 | devtool: 'source-map',
15 | }
16 | }
17 | };
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: 6.9.1
3 | before_script:
4 | - "export DISPLAY=:99.0"
5 | - "sh -e /etc/init.d/xvfb start"
6 | - sleep 3 # give xvfb some time to start
7 | script:
8 | - BROWSER=firefox gulp
9 | cache:
10 | yarn: true
11 | directories:
12 | - node_modules
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Pivotal Software, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm start
--------------------------------------------------------------------------------
/Procfile.dev:
--------------------------------------------------------------------------------
1 | web: gulp s
2 | jasmine: gulp jasmine
3 | assets: PORT=3000 gulp dev-server
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED
2 |
3 | Use [Create React App](https://github.com/facebook/create-react-app)
4 |
5 | # React Starter
6 |
7 | [](https://travis-ci.org/pivotal-cf/react-starter)
8 |
9 | React Starter is a todoApp project with much of the tooling in place you would need for a fully-featured React application.
10 | [Click here](http://react-starter.cfapps.io/) to see it in action.
11 |
12 | # Table of Contents
13 | 1. [Getting Started](#getting-started)
14 | 1. [Testing](#testing)
15 | 1. [Linting](#linting)
16 | 1. [Assets](#assets)
17 | 1. [Patterns](#patterns)
18 | 1. [Troubleshooting](#troubleshooting)
19 |
20 | ## Getting Started
21 |
22 | Install gulp:
23 | ```bash
24 | brew install gulp
25 | ```
26 |
27 | Checkout the project, install dependencies, and start foreman:
28 | ```bash
29 | git clone git@github.com:pivotal-cf/react-starter.git && cd react-starter
30 | npm install
31 | gulp foreman
32 | ```
33 |
34 | This will start up the development server at [3000](http://localhost:3000) and the Jasmine server at [8888](http://localhost:8888).
35 | The app includes example React architecture, along with Jasmine unit tests and a WebdriverIO integration test.
36 |
37 | ## Deploying
38 |
39 | To deploy to cloud foundry:
40 |
41 | 1. choose a unique name for your application and change `name: react-starter` in `manifest.yml` to your unique name
42 | 1. login to cf, target your org and space
43 | 1. `gulp deploy`
44 |
45 | Note that `cf push` by itself will not work. The `gulp deploy` task will compile your assets and configure the staticfile for the buildpack before doing `cf push`
46 |
47 | ## Testing
48 |
49 | ### Unit Testing
50 |
51 | Any files matching `spec/app/**/*_spec.js` will be run as part of [Jasmine](jasmine.github.io). There are some example tests included in `spec/app/components/`.
52 |
53 | To run the tests headlessly in phantomjs:
54 | ```
55 | gulp spec-app
56 | ```
57 |
58 | To run a Jasmine server (on port 8888):
59 | ```
60 | gulp jasmine
61 | ```
62 | The jasmine server will watch for file changes and update appropriately.
63 | Note that `gulp foreman` will start a jasmine server for you.
64 |
65 | In general, testing a React component will need the line `require('../spec_helper')` as the first line.
66 | The test will also probably have lines like
67 | ```
68 | const MyComponent = require('../../../app/components/my_component');
69 | ReactDom.render( , root)
70 | ```
71 | where `props` is an object representing the props passed into the React component.
72 | The spec_helper re-creates a div with id="root" (referenced by `root`) where you can render your components.
73 |
74 | Testing the results of rendering is made easier with [jasmine_dom_matchers](https://github.com/charleshansen/jasmine_dom_matchers),
75 | this is where `toHaveText` is defined.
76 |
77 | We have also provided some custom matchers with [pivotal-js-jasmine-matchers](https://github.com/pivotal-cf/pivotal-js/tree/master/packages/pivotal-js-jasmine-matchers).
78 |
79 | #### Factories
80 |
81 | React starter sets up Factories using [Rosie](https://github.com/rosiejs/rosie).
82 | Factories are defined in the `spec/factories` folder.
83 | The easiest way to create a new factory is to create a new file in `spec/factories`.
84 | See `spec/factories/user.js` as an example.
85 |
86 |
87 | ### Integration Testing
88 |
89 | Integration tests use [selenium-standalone](https://github.com/vvo/selenium-standalone) and [WebdriverIO](http://webdriver.io/).
90 |
91 | Selenium requires Java, so make sure this is installed. Run:
92 | ```
93 | gulp spec-integration
94 | ```
95 |
96 | This will take any files matching `spec/integration/**/*_spec.js` and run them through Jasmine.
97 | We provide a `describeWithWebdriver` function, inside of which you have access to WebdriverIO functionality.
98 |
99 | WebdriverIO is based on promises. Any time you interact with the browser in any way, this will be asynchronous and return a promise.
100 | To make this more readable, we use `async`/`await` syntax (from EcmaScript 2016) and the `done` callback from Jasmine.
101 |
102 | There are also a number of functions provided in `spec/integration/helpers/webdriver_helper.js`.
103 |
104 | An example integration test is provided at `spec/integration/features_spec.js`.
105 |
106 | ## Linting
107 |
108 | To lint your JavaScript code using [ESLint](http://eslint.org/):
109 |
110 | ```
111 | gulp lint
112 | ```
113 |
114 | The linting rules are set in `.eslintrc`
115 |
116 |
117 | ## Assets
118 |
119 | The JavaScript is compiled using [Babel](https://babeljs.io/) and [Webpack](https://webpack.github.io/).
120 | Additional webpack loaders and webpack plugins are used to compile the sass and html. By default, the entry point for your browser JavaScript is `app/index.js`.
121 |
122 | Webpack configurations are in `config/webpack/`. For example, if NODE_ENV is 'production', webpack is configured with `config/webpack/production.js`
123 |
124 | ```bash
125 | NODE_ENV=production gulp assets
126 | ```
127 | will output `application.js`, `application.css`, and `index.html` into the public folder.
128 | ```bash
129 | NODE_ENV=production gulp assets-config
130 | ```
131 | will output `config.js` into the public folder. These assets can then be served statically.
132 |
133 | React starter is in development mode if `NODE_ENV=development` or undefined.
134 | In development mode, the express server serves up `index.html`, `application.js` and `application.css`, using `webpack-dev-middleware`. `config.js` is served separately. This uses the webpack config in `config/webpack/development.js`
135 |
136 | ## Patterns
137 |
138 | #### Flux
139 |
140 | We have provided an example flux implementation in this application.
141 |
142 | * A component calls an action
143 | * The action calls the dispatcher
144 | * The corresponding method in the dispatcher updates the global store
145 |
146 | The flux patterns used in React starter have been extracted into [p-flux](https://github.com/pivotal-cf/p-flux).
147 | Look into p-flux documentation for best practices on storing and updating data.
148 |
149 | #### Router
150 |
151 | We have provided an example router in this application. The router is using [Grapnel](https://github.com/bytecipher/grapnel).
152 |
153 | Router callbacks should be responsible for changing the page.
154 | This can be accomplished by storing a page component in the router, as in `app/components/router.js`.
155 | Router callbacks also have access to props and Actions to save route params in the store.
156 |
157 | We recommend having a `setRoute` dispatch event for easy debugging. We have provided an example in `app/dispatchers/main_dispatcher.js`.
158 |
159 | We have also provided a mock router for use in test in `spec/app/support/mock_router.js`.
160 | The mock router is installed in `spec/app/spec_helper.js`.
161 | If you do not mock the router, it will change your browser URL while running Jasmine.
162 |
163 | #### API
164 |
165 | We have provided an example workflow that talks to an api, using the JSONPlaceholder api and `window.fetch`.
166 | Using an api requires asynchronous testing, which can be difficult.
167 | We use [MockPromises](https://github.com/charleshansen/mock-promises) to deal with most of it.
168 |
169 | ## Troubleshooting
170 |
171 | ### node
172 |
173 | React Starter requires:
174 | * Node version 4+ (it may work with older versions of node, but node-sass less likely to install correctly).
175 | * Npm version 3+
176 |
177 | If either of these is an earlier version, you will likely see errors when you run the code.
178 | If you have installed and then realize you need to change either of these, you will need to `rm -rf node_modules` and `npm install` to make sure dependencies are correctly updated and installed.
179 |
180 | Windows Users: To install node-sass, you will need a C compiler like Visual Studio installed, and probably also Python 2.x
181 |
--------------------------------------------------------------------------------
/Staticfile:
--------------------------------------------------------------------------------
1 | pushstate: enabled
2 |
3 |
--------------------------------------------------------------------------------
/app/api/fake_posts_api.js:
--------------------------------------------------------------------------------
1 | const {fetchJson} = require('../../helpers/fetch_helper');
2 |
3 | const apiUrl = 'http://jsonplaceholder.typicode.com';
4 |
5 | const FakePostsApi = {
6 | fetch() {
7 | return fetchJson(`${apiUrl}/posts`);
8 | }
9 | };
10 |
11 | module.exports = FakePostsApi;
--------------------------------------------------------------------------------
/app/components/api_page.js:
--------------------------------------------------------------------------------
1 | const {Actions} = require('p-flux');
2 | const React = require('react');
3 | import PropTypes from 'prop-types';
4 |
5 | class ApiPage extends React.Component {
6 | static propTypes = {
7 | posts: PropTypes.array
8 | };
9 |
10 | componentDidMount() {
11 | Actions.fetchPosts();
12 | }
13 |
14 | render() {
15 | const {posts = []} = this.props;
16 | const titles = posts.map(({title, id}) =>
{title}
);
17 | return (
18 |
19 |
This page talks to an api
20 | The api has posts with titles:
21 | {titles}
22 |
23 | );
24 | }
25 | }
26 |
27 | module.exports = ApiPage;
--------------------------------------------------------------------------------
/app/components/application.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | import PropTypes from 'prop-types';
3 | const {useStore} = require('p-flux');
4 | const {useRouter} = require('./use_router');
5 | const Router = require('./router');
6 |
7 | if (typeof document !== 'undefined') {
8 | require('../stylesheets/application.scss');
9 | }
10 |
11 | class Application extends React.Component {
12 | static propTypes = {
13 | config: PropTypes.object.isRequired,
14 | store: PropTypes.object.isRequired,
15 | router: PropTypes.oneOfType([PropTypes.object, PropTypes.func])
16 | };
17 |
18 | render() {
19 | const {config, store, router} = this.props;
20 | return (
21 |
31 | );
32 | }
33 | }
34 |
35 | const EnhancedApplication = useStore(useRouter(Application),
36 | {
37 | store: require('../store'),
38 | actions: [],
39 | dispatcherHandlers: [
40 | require('../dispatchers/main_dispatcher'),
41 | require('../dispatchers/api_dispatcher')
42 | ],
43 | /* eslint-disable no-console */
44 | onDispatch: (event) => {console.info('dispatching event', event);}
45 | /* eslint-enable no-console */
46 | }
47 | );
48 |
49 | module.exports = EnhancedApplication;
50 |
--------------------------------------------------------------------------------
/app/components/layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import ReactDOMServer from 'react-dom/server';
4 |
5 | export default function Layout({config, children}) {
6 | const configJs = `window.${config.globalNamespace} = {animation: true, config: ${JSON.stringify(config)}}`;
7 | const metas = Layout.metas.map((props, key) => );
8 | return (
9 |
10 | {metas}
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
19 | Layout.propTypes = {
20 | config: PropTypes.object.isRequired
21 | };
22 |
23 | Layout.metas = [
24 | {charSet: 'utf-8'},
25 | {httpEquiv: 'x-ua-compatible', content: 'ie=edge'},
26 | {name: 'description', content: ''},
27 | {name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no'}
28 | ];
29 |
--------------------------------------------------------------------------------
/app/components/router.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | import PropTypes from 'prop-types';
3 | const ApiPage = require('./api_page');
4 | const UserCreatePage = require('./user_create_page');
5 | const UserListPage = require('./user_list_page');
6 | const TodoPage = require('./todo_page');
7 |
8 | function isObject(obj) {
9 | return typeof obj === 'object';
10 | }
11 |
12 | function toFlattenedRoutes(routesHash) {
13 | return Object.keys(routesHash).reduce((paths, parent) => {
14 | if (isObject(routesHash[parent])) {
15 | const children = toFlattenedRoutes(routesHash[parent]);
16 | Object.keys(children).forEach(child => paths[parent + child] = children[child]);
17 | } else {
18 | paths[parent] = routesHash[parent];
19 | }
20 | return paths;
21 | }, {});
22 | }
23 |
24 | const ROUTES = {
25 | '/': 'todoList',
26 | '/todoList': 'todoList',
27 | '/apiPage': 'apiPage',
28 | '/users': {
29 | '/list': 'showUsers',
30 | '/new': 'createUser'
31 | }
32 | };
33 |
34 | const PAGES = { ApiPage, UserCreatePage, UserListPage, TodoPage };
35 |
36 | class Router extends React.Component {
37 | static propTypes = {
38 | router: PropTypes.oneOfType([PropTypes.object, PropTypes.func])
39 | };
40 |
41 | constructor(props, context) {
42 | super(props, context);
43 | const {state} = this;
44 | this.state = {...state, pageName: 'TodoPage' };
45 | }
46 |
47 | componentDidMount() {
48 | const {router} = this.props;
49 | Object.entries(toFlattenedRoutes(ROUTES)).map(([path, callbackName]) => {
50 | router.get(path, this[callbackName]);
51 | });
52 | }
53 |
54 | apiPage = () => {
55 | this.setState({pageName: 'ApiPage'});
56 | };
57 |
58 | todoList = () => {
59 | this.setState({pageName: 'TodoPage'});
60 | };
61 |
62 | showUsers = () => {
63 | this.setState({pageName: 'UserListPage'});
64 | };
65 |
66 | createUser = () => {
67 | this.setState({pageName: 'UserCreatePage'});
68 | };
69 |
70 | render() {
71 | const {pageName} = this.state;
72 | const Page = PAGES[pageName];
73 | return (
74 |
75 | );
76 | }
77 | }
78 |
79 | module.exports = Router;
80 |
--------------------------------------------------------------------------------
/app/components/todo_adder.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const {Actions} = require('p-flux');
3 |
4 | class TodoAdder extends React.Component{
5 | constructor(props, context) {
6 | super(props, context);
7 | this.state = {todoItem: ''};
8 | }
9 |
10 | submit = e => {
11 | e.preventDefault();
12 | Actions.todoItemCreate(this.state.todoItem);
13 | this.setState({todoItem: ''});
14 | };
15 |
16 | change = e => {
17 | this.setState({[e.currentTarget.name]: e.target.value});
18 | };
19 |
20 | render() {
21 | const {todoItem} = this.state;
22 |
23 | return (
24 |
25 |
29 |
30 | );
31 | }
32 | };
33 |
34 | module.exports = TodoAdder;
35 |
--------------------------------------------------------------------------------
/app/components/todo_item.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | import PropTypes from 'prop-types';
3 |
4 | class TodoItem extends React.Component{
5 | static propTypes = {
6 | value: PropTypes.node.isRequired
7 | };
8 |
9 | render() {
10 | const {value} = this.props;
11 | return (
12 |
13 | {value}
14 |
15 | );
16 | }
17 | }
18 |
19 | module.exports = TodoItem;
20 |
--------------------------------------------------------------------------------
/app/components/todo_list.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const TodoItem = require('./todo_item');
3 | import PropTypes from 'prop-types';
4 |
5 | class TodoList extends React.Component {
6 | static propTypes = {
7 | todoItems: PropTypes.array.isRequired
8 | };
9 |
10 | render() {
11 | const {todoItems} = this.props;
12 | const todoItemsList = todoItems.map((item, key) => ());
13 |
14 | return (
15 |
16 | {todoItemsList}
17 |
18 | );
19 | }
20 | }
21 |
22 | module.exports = TodoList;
23 |
--------------------------------------------------------------------------------
/app/components/todo_page.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | import PropTypes from 'prop-types';
3 | const TodoAdder = require('./todo_adder');
4 | const TodoList = require('./todo_list');
5 |
6 | class TodoPage extends React.Component {
7 | static propTypes = {
8 | config: PropTypes.object,
9 | todoItems: PropTypes.array
10 | };
11 |
12 | render() {
13 | const {config: {title}, todoItems} = this.props;
14 | return (
15 |
16 |
{title}
17 | Things to do
18 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | module.exports = TodoPage;
26 |
--------------------------------------------------------------------------------
/app/components/use_router.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const Grapnel = require('grapnel');
3 | const {Dispatcher} = require('p-flux');
4 |
5 | const exports = {
6 | Router: Grapnel,
7 | useRouter: (Component) => class extends React.Component {
8 | constructor(props, context) {
9 | super(props, context);
10 | const {state} = this;
11 | const router = new (exports.Router)({pushState: true});
12 | Dispatcher.router = router;
13 | this.state = {...state, router};
14 | }
15 |
16 | render() {
17 | return ( );
18 | }
19 | }
20 | };
21 |
22 | module.exports = exports;
23 |
--------------------------------------------------------------------------------
/app/components/user_create_page.js:
--------------------------------------------------------------------------------
1 | const {Actions} = require('p-flux');
2 | const React = require('react');
3 |
4 | class UserCreatePage extends React.Component {
5 | constructor(props, context) {
6 | super(props, context);
7 | this.state = {userName: ''};
8 | }
9 |
10 | submit = e => {
11 | e.preventDefault();
12 | Actions.userCreate({name: this.state.userName});
13 | this.setState({userName: ''});
14 | Actions.setRoute('/users/list');
15 | };
16 |
17 | change = e => {
18 | this.setState({[e.currentTarget.name]: e.target.value});
19 | };
20 |
21 | render() {
22 | const {userName} = this.state;
23 | return (
24 |
25 |
Create a User!
26 |
30 |
31 | );
32 | }
33 | }
34 |
35 | module.exports = UserCreatePage;
36 |
--------------------------------------------------------------------------------
/app/components/user_list_page.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | import PropTypes from 'prop-types';
3 |
4 | class UserListPage extends React.Component {
5 | static propTypes = {
6 | users: PropTypes.array
7 | };
8 |
9 | render() {
10 | const {users} = this.props;
11 | const userItems = users.map((user, key) => User name: {user.name} );
12 | return (
13 |
14 |
List of Users
15 |
16 |
17 | );
18 | }
19 | }
20 |
21 | module.exports = UserListPage;
22 |
--------------------------------------------------------------------------------
/app/config.js:
--------------------------------------------------------------------------------
1 | const config = require('../pui-react-tools/config')();
2 |
3 | const {globalNamespace = 'Application'} = config;
4 |
5 | module.exports = (function() {
6 | `window.${globalNamespace} = {config: ${config}}`;
7 | })();
8 |
--------------------------------------------------------------------------------
/app/dispatchers/api_dispatcher.js:
--------------------------------------------------------------------------------
1 | const FakePostsApi = require('../api/fake_posts_api');
2 |
3 | const ApiDispatcher = {
4 | fetchPosts(){
5 | return FakePostsApi.fetch().then((data) => {
6 | this.dispatch({type: 'updatePosts', data});
7 | });
8 | },
9 | updatePosts({data}){
10 | this.$store.merge({posts: data});
11 | }
12 | };
13 |
14 | module.exports = ApiDispatcher;
15 |
--------------------------------------------------------------------------------
/app/dispatchers/main_dispatcher.js:
--------------------------------------------------------------------------------
1 | const MainDispatcher = {
2 | setRoute({data}) {
3 | this.router.navigate(data);
4 | },
5 | todoItemCreate({data}) {
6 | this.$store.refine('todoItems').push(data);
7 | },
8 | userCreate({data}) {
9 | this.$store.refine('users').push(data);
10 | },
11 | userSet({data}) {
12 | this.$store.merge({userId: data});
13 | }
14 | };
15 |
16 | module.exports = MainDispatcher;
17 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | const invariant = require('invariant');
2 | const React = require('react');
3 | const ReactDOM = require('react-dom');
4 | const Application = require('./components/application');
5 | const {AppContainer} = require('react-hot-loader');
6 |
7 | invariant(global.MyReactStarter,
8 | `globalNamespace in application.json has been changed without updating global variable name.
9 | Please change "MyReactStarter" in app/index.js to your current globalNamespace`
10 | );
11 |
12 | const {config} = global.MyReactStarter;
13 | ReactDOM.render(
14 |
15 |
16 | , root
17 | );
18 |
19 | if (module.hot) {
20 | module.hot.accept('./components/application', () => {
21 | const NextApp = require('./components/application');
22 | ReactDOM.render(
23 |
24 |
25 | ,
26 | root
27 | );
28 | });
29 | }
--------------------------------------------------------------------------------
/app/index.jsx:
--------------------------------------------------------------------------------
1 | import ReactDOMServer from 'react-dom/server';
2 | import Layout from './components/layout';
3 | import React from 'react';
4 | import Application from './components/application';
5 |
6 | export default function Index(props, done) {
7 | const {config = {}} = props;
8 | const html = `${ReactDOMServer.renderToStaticMarkup( )}`;
9 | if (!done) return html;
10 | done(null, html);
11 | }
--------------------------------------------------------------------------------
/app/store.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | todoItems: [],
3 | users: []
4 | };
5 |
--------------------------------------------------------------------------------
/app/stylesheets/_layout.scss:
--------------------------------------------------------------------------------
1 | .pui-react-starter {
2 | padding: 20px;
3 | color: darkblue;
4 | }
--------------------------------------------------------------------------------
/app/stylesheets/application.scss:
--------------------------------------------------------------------------------
1 | @import '_layout';
2 |
--------------------------------------------------------------------------------
/app/stylesheets/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'autoprefixer': {},
4 | }
5 | };
--------------------------------------------------------------------------------
/config/application.json:
--------------------------------------------------------------------------------
1 | {
2 | "globalNamespace": "MyReactStarter",
3 | "title": "My React Starter"
4 | }
5 |
--------------------------------------------------------------------------------
/config/development.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/config/env.json:
--------------------------------------------------------------------------------
1 | [
2 | "FOO"
3 | ]
4 |
--------------------------------------------------------------------------------
/config/integration.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | }
--------------------------------------------------------------------------------
/config/production.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | }
--------------------------------------------------------------------------------
/config/test.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/config/webpack/development.js:
--------------------------------------------------------------------------------
1 | import NoEmitOnErrorsPlugin from 'webpack/lib/NoEmitOnErrorsPlugin';
2 | import HtmlWebpackPlugin from 'html-webpack-plugin';
3 | import HtmlWebpackIncludeAssetsPlugin from 'html-webpack-include-assets-plugin';
4 | import ManifestPlugin from 'webpack-manifest-plugin';
5 | import HotModuleReplacementPlugin from 'webpack/lib/HotModuleReplacementPlugin';
6 |
7 | export default function() {
8 | return {
9 | devServer: {
10 | hot: true,
11 | proxy: {
12 | '/config.js*': {
13 | target: `http://localhost:${process.env.API_PORT || 3001}`
14 | },
15 | '*': {
16 | bypass: () => '/index.html'
17 | }
18 | }
19 | },
20 | cache: true,
21 | devtool: 'source-map',
22 | entry: {
23 | application: [
24 | 'babel-polyfill',
25 | 'react-hot-loader/patch',
26 | `webpack-dev-server/client?http://localhost:${process.env.PORT || 3000}`,
27 | 'webpack/hot/only-dev-server',
28 | './app/index.js'
29 | ],
30 | },
31 | module: {
32 | rules: [
33 | {
34 | test: [/\.eot(\?|$)/, /\.ttf(\?|$)/, /\.woff2?(\?|$)/, /\.png(\?|$)/, /\.gif(\?|$)/, /\.jpe?g(\?|$)/],
35 | exclude: /node_modules/,
36 | use: {loader: 'file-loader?name=[name].[ext]'}
37 | },
38 | {
39 | test: /\.s?css$/,
40 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
41 | },
42 | {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'}
43 | ]
44 | },
45 | output: {filename: '[name].js', chunkFilename: '[id].js', pathinfo: true, publicPath: '/'},
46 | plugins: [
47 | new NoEmitOnErrorsPlugin(),
48 | new HtmlWebpackPlugin({title: 'ReactStarter', template: 'app/index.jsx'}),
49 | new HtmlWebpackIncludeAssetsPlugin({ assets: ['config.js'], append: false, hash: true}),
50 | new ManifestPlugin(),
51 | new HotModuleReplacementPlugin()
52 | ]
53 | };
54 | };
--------------------------------------------------------------------------------
/config/webpack/production.js:
--------------------------------------------------------------------------------
1 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
2 | import ManifestPlugin from 'webpack-manifest-plugin';
3 | import NoEmitOnErrorsPlugin from 'webpack/lib/NoEmitOnErrorsPlugin';
4 | import DefinePlugin from 'webpack/lib/DefinePlugin';
5 | import HtmlWebpackIncludeAssetsPlugin from 'html-webpack-include-assets-plugin';
6 | import HtmlWebpackPlugin from 'html-webpack-plugin';
7 | import UglifyJsPlugin from 'webpack/lib/optimize/UglifyJsPlugin';
8 |
9 | export default function() {
10 | return {
11 | entry: {
12 | application: ['babel-polyfill', './app/index.js']
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: [/\.eot(\?|$)/, /\.ttf(\?|$)/, /\.woff2?(\?|$)/, /\.png(\?|$)/, /\.gif(\?|$)/, /\.jpe?g(\?|$)/],
18 | exclude: /node_modules/,
19 | use: {loader: 'file-loader?name=[name]-[hash].[ext]'}
20 | },
21 | {
22 | test: /\.s?css$/,
23 | oneOf: [
24 | {test: /html-webpack-plugin/, use: 'null-loader'},
25 | {use: ExtractTextPlugin.extract({fallbackLoader: 'style-loader', loader: ['css-loader', 'postcss-loader', 'sass-loader']})}
26 | ]
27 | },
28 | {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'}
29 | ]
30 | },
31 | output: {filename: '[name]-[hash].js', chunkFilename: '[id].js', publicPath: '/'},
32 | plugins: [
33 | new DefinePlugin({'process.env': {'NODE_ENV': '"production"'}}),
34 | new NoEmitOnErrorsPlugin(),
35 | new HtmlWebpackPlugin({title: 'ReactStarter', template: 'app/index.jsx'}),
36 | new HtmlWebpackIncludeAssetsPlugin({ assets: ['config.js'], append: false, hash: true}),
37 | new ManifestPlugin(),
38 | new ExtractTextPlugin({filename: '[name]-[hash].css'}),
39 | new UglifyJsPlugin({
40 | compressor: {screw_ie8: true, warnings: false},
41 | mangle: {screw_ie8: true},
42 | output: {comments: false, screw_ie8: true}
43 | })
44 | ],
45 | stats: {colors: true, cached: false}
46 | };
47 | };
--------------------------------------------------------------------------------
/config/webpack/test.js:
--------------------------------------------------------------------------------
1 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
2 | import NoEmitOnErrorsPlugin from 'webpack/lib/NoEmitOnErrorsPlugin';
3 |
4 | export default function() {
5 | return {
6 | cache: true,
7 | devtool: 'source-map',
8 | entry: {spec: './spec/app/index.js'},
9 | module: {
10 | rules: [
11 | {
12 | test: [/\.eot(\?|$)/, /\.ttf(\?|$)/, /\.woff2?(\?|$)/, /\.png(\?|$)/, /\.gif(\?|$)/, /\.jpe?g(\?|$)/],
13 | exclude: /node_modules/,
14 | use: {loader: 'file-loader?name=[name].[ext]'}
15 | },
16 | {
17 | test: /\.s?css$/,
18 | use: ExtractTextPlugin.extract({fallbackLoader: 'style-loader', loader: ['css-loader', 'postcss-loader', 'sass-loader']})
19 | },
20 | {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'}
21 | ]
22 | },
23 | output: {filename: '[name].js', chunkFilename: '[id].js'},
24 | plugins: [
25 | new NoEmitOnErrorsPlugin(),
26 | new ExtractTextPlugin({filename: '[name].css'}),
27 | ],
28 | watch: true
29 | };
30 | };
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | require('babel-polyfill');
3 |
4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development';
5 |
6 | const requireDir = require('require-dir');
7 | requireDir('./tasks');
--------------------------------------------------------------------------------
/helpers/application_helper.js:
--------------------------------------------------------------------------------
1 | const helpers = {
2 | compact(array) {
3 | return array.filter(Boolean);
4 | }
5 | };
6 |
7 | module.exports = helpers;
8 |
--------------------------------------------------------------------------------
/helpers/fetch_helper.js:
--------------------------------------------------------------------------------
1 | function checkStatus(response) {
2 | if (response.status >= 200 && response.status < 400) return response;
3 | const error = new Error(response.statusText);
4 | error.response = response;
5 | throw error;
6 | }
7 |
8 | module.exports = {
9 | fetchJson(url, {accessToken, headers, ...options} = {}) {
10 | require('isomorphic-fetch');
11 | const acceptHeaders = {accept: 'application/json', 'Content-Type': 'application/json'};
12 | const authorizationHeaders = accessToken ? {authorization: `Bearer ${accessToken}`} : {};
13 | options = {credentials: 'same-origin', headers: {...acceptHeaders, ...authorizationHeaders, ...headers}, ...options};
14 | return fetch(url, options)
15 | .then(checkStatus)
16 | .then(response => [204, 304].includes(response.status) ? {} : response.json());
17 | }
18 | };
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* eslint-disable no-var */
3 | var recluster = require('recluster');
4 | var path = require('path');
5 | var cluster = recluster(path.join(__dirname, 'server', 'bootstrap.js'), {readyWhen: 'ready', workers: 1});
6 | /* eslint-enable no-var */
7 | cluster.run();
8 | process.on('SIGUSR2', function() {
9 | console.log('Got SIGUSR2, reloading cluster...');
10 | cluster.reload();
11 | });
12 | console.log('spawned cluster, kill -s SIGUSR2', process.pid, 'to reload');
13 |
--------------------------------------------------------------------------------
/manifest.yml:
--------------------------------------------------------------------------------
1 | ---
2 | applications:
3 | - name: react-starter
4 | path: public
5 | memory: 64MB
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-starter",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "index.js",
6 | "devDependencies": {
7 | "autoprefixer": "^7.1.3",
8 | "babel-core": "^6.18.2",
9 | "babel-loader": "^7.1.2",
10 | "babel-plugin-add-module-exports": "^0.2.1",
11 | "babel-plugin-transform-object-assign": "^6.8.0",
12 | "babel-plugin-transform-react-display-name": "^6.8.0",
13 | "babel-polyfill": "^6.16.0",
14 | "babel-preset-es2015": "^6.18.0",
15 | "babel-preset-react": "^6.16.0",
16 | "babel-preset-stage-0": "^6.16.0",
17 | "bluebird": "^3.4.6",
18 | "classnames": "^2.2.5",
19 | "css-loader": "^0.28.5",
20 | "del": "^3.0.0",
21 | "event-stream": "^3.3.4",
22 | "express": "^4.14.0",
23 | "extract-text-webpack-plugin": "git+https://git@github.com/digitalkaoz/extract-text-webpack-plugin#5b082b0bac0e068e9246d67c5cf6edeac3a5675b",
24 | "file-loader": "^0.11.2",
25 | "foreman": "^2.0.0",
26 | "from2": "^2.3.0",
27 | "grapnel": "^0.6.4",
28 | "gulp": "^3.9.1",
29 | "gulp-jasmine": "^2.4.2",
30 | "gulp-load-plugins": "^1.4.0",
31 | "gulp-plumber": "^1.1.0",
32 | "gulp-util": "^3.0.8",
33 | "html-webpack-include-assets-plugin": "^0.0.7",
34 | "html-webpack-plugin": "^2.28.0",
35 | "invariant": "^2.2.1",
36 | "isomorphic-fetch": "^2.2.1",
37 | "jasmine-ajax": "^3.2.0",
38 | "jasmine-async-suite": "^0.0.8",
39 | "jasmine_dom_matchers": "^1.4.0",
40 | "jquery": "^3.1.1",
41 | "merge-stream": "^1.0.0",
42 | "mock-promises": "^0.8.0",
43 | "node-sass": "^4.5.3",
44 | "npm": "^5.3.0",
45 | "null-loader": "^0.1.1",
46 | "p-flux": "^1.1.0",
47 | "phantomjs-prebuilt": "^2.1.13",
48 | "pivotal-js-jasmine-matchers": "^0.1.1",
49 | "pivotal-js-react-test-helpers": "^0.1.0",
50 | "portastic": "^1.0.1",
51 | "postcss-loader": "^2.0.6",
52 | "prop-types": "^15.5.10",
53 | "pui-cursor": "^3.0.4",
54 | "pui-react-tools": "^3.0.2",
55 | "qs": "^6.5.0",
56 | "react": "^15.6.1",
57 | "react-dom": "^15.6.1",
58 | "react-hot-loader": "^3.0.0-beta.6",
59 | "recluster": "^0.4.5",
60 | "require-dir": "^0.3.2",
61 | "rosie": "^1.6.0",
62 | "run-sequence": "^2.1.0",
63 | "sass-loader": "^6.0.6",
64 | "selenium-standalone": "^5.11.0",
65 | "strong-wait-till-listening": "^1.0.3",
66 | "style-loader": "^0.18.2",
67 | "thenify": "^3.2.1",
68 | "url-join": "^2.0.2",
69 | "vinyl": "^2.0.2",
70 | "webdriverio": "^4.3.0",
71 | "webpack": "^3.5.5",
72 | "webpack-dev-server": "^2.6.1",
73 | "webpack-manifest-plugin": "^1.1.0"
74 | },
75 | "scripts": {
76 | "test": "./node_modules/.bin/gulp spec",
77 | "start": "node index.js"
78 | },
79 | "repository": {
80 | "type": "git",
81 | "url": "https://github.com/pivotal-cf/pui-react-starter.git"
82 | },
83 | "author": "",
84 | "license": "MIT",
85 | "bugs": {
86 | "url": "https://github.com/pivotal-cf/pui-react-starter/issues"
87 | },
88 | "homepage": "https://github.com/pivotal-cf/pui-react-starter"
89 | }
90 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 |
3 | export default function (config) {
4 | const app = express();
5 |
6 | app.get('/config.js', (req, res) => {
7 | res.type('text/javascript').status(200)
8 | .send(`window.${config.globalNamespace} = {config: ${JSON.stringify(config)}}`);
9 | });
10 |
11 | return app;
12 | };
--------------------------------------------------------------------------------
/server/bootstrap.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | require('babel-polyfill');
3 |
4 | /* eslint-disable no-var */
5 | let app = require('./app')(require('pui-react-tools/assets/config')());
6 | /* eslint-enable no-var */
7 | let apiPort = process.env.API_PORT || process.env.PORT || 3001;
8 | /* eslint-disable no-console */
9 | console.log(`API listening on ${apiPort}`);
10 | /* eslint-enable no-console */
11 |
12 | app.listen(apiPort, function() {
13 | process.send && process.send({cmd: 'ready'});
14 | });
15 |
16 | module.exports = app;
17 |
--------------------------------------------------------------------------------
/server/env.js:
--------------------------------------------------------------------------------
1 | module.exports = function() {
2 | try {
3 | Object.entries(require('../.env.json'))
4 | .filter(([key]) => !(key in process.env))
5 | .forEach(([key, value]) => process.env[key] = value);
6 | } catch(e) {
7 | }
8 | };
--------------------------------------------------------------------------------
/spec/app/api/fake_posts_api_spec.js:
--------------------------------------------------------------------------------
1 | require('../../spec_helper');
2 |
3 | describe('FakePostsApi', () => {
4 | let subject;
5 | const apiUrl = 'http://jsonplaceholder.typicode.com';
6 |
7 |
8 | beforeEach(() => {
9 | subject = require('../../../app/api/fake_posts_api');
10 | });
11 |
12 | describe('#fetch', () => {
13 | let doneSpy, failSpy, request;
14 | beforeEach(() => {
15 | doneSpy = jasmine.createSpy('done');
16 | failSpy = jasmine.createSpy('fail');
17 | subject.fetch().then(doneSpy, failSpy);
18 | request = jasmine.Ajax.requests.mostRecent();
19 | });
20 |
21 | it('requests users', () => {
22 | expect(`${apiUrl}/posts`).toHaveBeenRequested();
23 | });
24 |
25 | describe('when the request is successful', () => {
26 | let response;
27 | beforeEach(() => {
28 | response = [
29 | {
30 | userId: 1,
31 | id: 1,
32 | title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
33 | body: 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto'
34 | },
35 | {
36 | userId: 1,
37 | id: 2,
38 | title: 'qui est esse',
39 | body: 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla'
40 | }];
41 | request.succeed(response);
42 | //The fetchJson method used by FakePostsApi has layers of '.then' attached to the fetch promise
43 | MockPromises.tick(4);
44 | });
45 |
46 | it('resolves the promise with the app', () => {
47 | expect(doneSpy).toHaveBeenCalledWith(response);
48 | });
49 | });
50 |
51 | describe('when the request is not successful', () => {
52 | beforeEach(() => {
53 | request.fail();
54 | MockPromises.tick(3);
55 | });
56 |
57 | it('rejects the promise', () => {
58 | expect(failSpy).toHaveBeenCalled();
59 | });
60 | });
61 | });
62 | });
--------------------------------------------------------------------------------
/spec/app/components/api_page_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('ApiPage', () => {
4 | beforeEach(() => {
5 | const ApiPage = require('../../../app/components/api_page');
6 | ReactDOM.render( , root);
7 | });
8 |
9 | it('fetches posts', () => {
10 | expect('fetchPosts').toHaveBeenDispatched();
11 | });
12 |
13 | it('renders the post titles result from the api', () => {
14 | expect('.api-page').toContainText('bar');
15 | expect('.api-page').toContainText('baz');
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/spec/app/components/application_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('Application', () => {
4 | let TodoList;
5 |
6 | beforeEach(() => {
7 | const Application = require('../../../app/components/application');
8 | TodoList = require('../../../app/components/todo_list');
9 | spyOn(TodoList.prototype, 'render').and.callThrough();
10 | const config = {title: 'title'};
11 | ReactDOM.render( , root);
12 | });
13 |
14 | it('has a TodoAdder', () => {
15 | expect('.todo-adder').toExist();
16 | });
17 |
18 | it('has a TodoList', () => {
19 | expect('.todo-list').toExist();
20 | });
21 |
22 | it('has a title', () => {
23 | expect('.title').toHaveText('title');
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/spec/app/components/router_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('Router', () => {
4 | beforeEach(() => {
5 | const Router = require('../../../app/components/router');
6 | const routerProps = {
7 | router: new MockRouter(),
8 | config: {},
9 | ...require('../../../app/store')
10 | };
11 | ReactDOM.render( , root);
12 | });
13 |
14 | describe('/', () => {
15 | beforeEach(() => {
16 | MockRouter.navigate('/');
17 | });
18 |
19 | it('renders a todo list', () => {
20 | expect('.todo-list').toExist();
21 | });
22 | });
23 |
24 | describe('/todoList', () => {
25 | beforeEach(() => {
26 | MockRouter.navigate('/todoList');
27 | });
28 |
29 | it('renders a todo list', () => {
30 | expect('.todo-list').toExist();
31 | });
32 | });
33 |
34 | describe('/apiPage', () => {
35 | beforeEach(() => {
36 | MockRouter.navigate('/apiPage');
37 | });
38 |
39 | it('renders a todo list', () => {
40 | expect('.api-page').toExist();
41 | });
42 | });
43 |
44 | describe('/users/list', () => {
45 | beforeEach(() => {
46 | MockRouter.navigate('/users/list');
47 | });
48 |
49 | it('renders the user list page', () => {
50 | expect('.user-list-page').toExist();
51 | });
52 | });
53 |
54 | describe('/users/new', () => {
55 | beforeEach(() => {
56 | MockRouter.navigate('/users/new');
57 | });
58 |
59 | it('renders the new user page', () => {
60 | expect('.user-create-page').toExist();
61 | });
62 | });
63 | });
--------------------------------------------------------------------------------
/spec/app/components/todo_adder_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('TodoAdder', () => {
4 | beforeEach(() => {
5 | const TodoAdder = require('../../../app/components/todo_adder');
6 | ReactDOM.render( , root);
7 | });
8 |
9 | describe('when adding a todo item', () => {
10 | beforeEach(() => {
11 | $('.todo-adder input').val('do this thing').simulate('change');
12 | $('.todo-adder form').simulate('submit');
13 | });
14 |
15 | it('adds the todoItem', () => {
16 | expect('todoItemCreate').toHaveBeenDispatchedWith({data: 'do this thing'});
17 | });
18 |
19 | it('clears out the input text', () => {
20 | expect('.todo-adder input').toHaveValue('');
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/spec/app/components/todo_item_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('TodoItem', () => {
4 | beforeEach(() => {
5 | const TodoItem = require('../../../app/components/todo_item');
6 | ReactDOM.render( , root);
7 | });
8 |
9 | it('renders the value of the todoitem', () => {
10 | expect('.todo-item').toHaveText('hey');
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/spec/app/components/todo_list_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('TodoList', () => {
4 | beforeEach(() => {
5 | const TodoList = require('../../../app/components/todo_list');
6 | ReactDOM.render( , root);
7 | });
8 |
9 | it('renders the todolist', () => {
10 | expect('.todo-item').toHaveLength(2);
11 | expect('.todo-item:eq(0)').toHaveText('do this');
12 | expect('.todo-item:eq(1)').toHaveText('do that');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/spec/app/components/todo_page_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('TodoPage', () => {
4 | beforeEach(() => {
5 | const TodoPage = require('../../../app/components/todo_page');
6 | ReactDOM.render( , root);
7 | });
8 |
9 | it('renders a title', () => {
10 | expect('.title').toHaveText('the title');
11 | });
12 |
13 | it('renders the todolist', () => {
14 | expect('.todo-item').toHaveLength(2);
15 | expect('.todo-item:eq(0)').toHaveText('do this');
16 | expect('.todo-item:eq(1)').toHaveText('do that');
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/spec/app/components/use_router_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 | import PropTypes from 'prop-types';
3 |
4 | describe('#useRouter', () => {
5 | let routeSpy;
6 |
7 | beforeEach(() => {
8 | const {useRouter} = require('../../../app/components/use_router');
9 | routeSpy = jasmine.createSpy('route');
10 |
11 | const Application = ({router}) => {
12 | router.get('/test', routeSpy);
13 | return (
14 |
15 | router.navigate('/test')}>Route
16 |
17 | );
18 | };
19 | Application.propTypes = {
20 | router: PropTypes.func.isRequired
21 | };
22 |
23 | const TestRouter = useRouter(Application);
24 | ReactDOM.render( , root);
25 | });
26 |
27 | it('routes', () => {
28 | $('.application button').simulate('click');
29 | expect(routeSpy).toHaveBeenCalled();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/spec/app/components/user_create_page_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('UserCreatePage', () => {
4 | beforeEach(() => {
5 | const UserCreatePage = require('../../../app/components/user_create_page');
6 | ReactDOM.render( , root);
7 | });
8 |
9 | describe('creating a user', () => {
10 | beforeEach(() => {
11 | $('.user-create-page input').val('Alice').simulate('change');
12 | $('.user-create-page form').simulate('submit');
13 | });
14 |
15 | it('creates a new user', () => {
16 | expect('userCreate').toHaveBeenDispatchedWith({data: {name: 'Alice'}});
17 | });
18 |
19 | it('navigates to the user list page', () => {
20 | expect('setRoute').toHaveBeenDispatchedWith({data: '/users/list'});
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/spec/app/components/user_list_page_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('UserListPage', () => {
4 | beforeEach(() => {
5 | const UserListPage = require('../../../app/components/user_list_page');
6 | const users = [Factory.build('user', {name: 'Felix'})];
7 | ReactDOM.render( , root);
8 | });
9 |
10 | it('renders the users', () => {
11 | expect('.user-list').toContainText('Felix');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/spec/app/dispatchers/api_dispatcher_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('ApiDispatcher', () => {
4 | let subject, Cursor, cursorSpy;
5 |
6 | beforeEach(() => {
7 | Cursor = require('pui-cursor');
8 | cursorSpy = jasmine.createSpy('callback');
9 | subject = Dispatcher;
10 |
11 | //dispatch is spied on in spec_helper
12 | subject.dispatch.and.callThrough();
13 |
14 | //prevent console logs
15 | spyOn(subject, 'onDispatch');
16 | });
17 |
18 | describe('fetchPosts', () => {
19 | const apiUrl = 'http://jsonplaceholder.typicode.com';
20 |
21 | beforeEach(() => {
22 | subject.$store = new Cursor({}, cursorSpy);
23 | subject.dispatch({type: 'fetchPosts'});
24 | });
25 |
26 | it('makes an api request to posts', () => {
27 | expect(`${apiUrl}/posts`).toHaveBeenRequested();
28 | });
29 |
30 | it('triggers updatePosts on success', () => {
31 | const request = jasmine.Ajax.requests.mostRecent();
32 | request.succeed(['bar', 'baz']);
33 | MockPromises.tick(4);
34 | expect('updatePosts').toHaveBeenDispatchedWith({data: ['bar', 'baz']});
35 | });
36 | });
37 |
38 | describe('updatePosts', () => {
39 | beforeEach(() => {
40 | subject.$store = new Cursor({posts: []}, cursorSpy);
41 | subject.dispatch({type: 'updatePosts', data: ['bar', 'baz']});
42 | });
43 |
44 | it('sets posts in the cursor', () => {
45 | expect(cursorSpy).toHaveBeenCalledWith({posts: ['bar', 'baz']});
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/spec/app/dispatchers/main_dispatcher_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('MainDispatcher', () => {
4 | let subject, Cursor, cursorSpy;
5 |
6 | beforeEach(() => {
7 | Cursor = require('pui-cursor');
8 | cursorSpy = jasmine.createSpy('callback');
9 | subject = Dispatcher;
10 |
11 | //dispatch is spied on in spec_helper
12 | subject.dispatch.and.callThrough();
13 |
14 | //prevent console logs
15 | spyOn(subject, 'onDispatch');
16 | });
17 |
18 | describe('todoItem', () => {
19 | beforeEach(() => {
20 | subject.$store = new Cursor({todoItems: []}, cursorSpy);
21 | });
22 |
23 | describe('create', () => {
24 | it('adds an item to the list of todos', () => {
25 | subject.dispatch({type: 'todoItemCreate', data: 'buy ham'});
26 | expect(cursorSpy).toHaveBeenCalledWith({
27 | todoItems: ['buy ham']
28 | });
29 | });
30 | });
31 | });
32 |
33 | describe('userCreate', () => {
34 | beforeEach(() => {
35 | subject.$store = new Cursor({users: [{name: 'Alice'}]}, cursorSpy);
36 | });
37 |
38 | it('adds a user to the list of users', () => {
39 | subject.dispatch({type: 'userCreate', data: {name: 'Bob'}});
40 | expect(cursorSpy).toHaveBeenCalledWith({
41 | users: [
42 | {name: 'Alice'},
43 | {name: 'Bob'}
44 | ]
45 | });
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/spec/app/index.js:
--------------------------------------------------------------------------------
1 | const specs = require.context('../app', true, /_spec\.js$/);
2 | specs.keys().forEach(specs);
3 |
--------------------------------------------------------------------------------
/spec/app/spec_helper.js:
--------------------------------------------------------------------------------
1 | require('jasmine-ajax');
2 | require('jasmine_dom_matchers');
3 | require('../support/bluebird');
4 | require('../spec_helper');
5 | require('pivotal-js-jasmine-matchers');
6 | require('./support/dispatcher_matchers');
7 |
8 |
9 | const factories = require.context('../factories', true, /\.js$/);
10 | factories.keys().forEach(factories);
11 |
12 | const Cursor = require('pui-cursor');
13 | const Deferred = require('../support/deferred');
14 | const {Dispatcher} = require('p-flux');
15 | const jQuery = require('jquery');
16 | const MockFetch = require('../support/mock_fetch');
17 | const MockPromises = require('mock-promises');
18 | const MockRouter = require('./support/mock_router');
19 | const React = require('react');
20 | const ReactDOM = require('react-dom');
21 | const UseRouter = require('../../app/components/use_router');
22 |
23 | let globals;
24 |
25 | MockFetch.install();
26 |
27 | beforeAll(() => {
28 | globals = {
29 | Deferred,
30 | Dispatcher,
31 | jQuery,
32 | MockPromises,
33 | MockRouter,
34 | MyReactStarter: {},
35 | React,
36 | ReactDOM,
37 | $: jQuery,
38 | ...require('pivotal-js-react-test-helpers')
39 | };
40 | Object.assign(global, globals);
41 | });
42 |
43 | afterAll(() => {
44 | Object.keys(globals).forEach(key => delete global[key]);
45 | MockFetch.uninstall();
46 | });
47 |
48 | beforeEach(() => {
49 | global.MyReactStarter = {config: {}};
50 |
51 | $('body').find('#root').remove().end().append('
');
52 | Cursor.async = false;
53 |
54 | const Application = require('../../app/components/application');
55 | Application.reset();
56 |
57 | spyOn(Dispatcher, 'dispatch');
58 |
59 | MockPromises.install(Promise);
60 | MockRouter.install(UseRouter);
61 |
62 | jasmine.clock().install();
63 | jasmine.Ajax.install();
64 | Object.assign(XMLHttpRequest.prototype, {
65 | succeed(data = {}, options = {}) {
66 | this.respondWith(Object.assign({status: 200, responseText: data ? JSON.stringify(data) : ''}, options));
67 | },
68 | fail(data, options = {}) {
69 | this.respondWith(Object.assign({status: 400, responseText: JSON.stringify(data)}, options));
70 | }
71 | });
72 | });
73 |
74 | afterEach(() => {
75 | ReactDOM.unmountComponentAtNode(root);
76 | Dispatcher.reset();
77 | MockPromises.uninstall();
78 | MockRouter.uninstall();
79 | jasmine.clock().uninstall();
80 | jasmine.Ajax.uninstall();
81 | });
82 |
--------------------------------------------------------------------------------
/spec/app/support/dispatcher_matchers.js:
--------------------------------------------------------------------------------
1 | function objectDiffs(actual, expected, util, customEqualityTesters) {
2 | function getDiffEntries() {
3 | return Object.entries(expected).filter(([key, value]) => {
4 | return !util.equals(actual[key], value, customEqualityTesters);
5 | });
6 | }
7 |
8 | function formatDiff([key, value]) {
9 | return `key: ${key}, \nexpected: ${jasmine.pp(value)}, \nfound : ${jasmine.pp(actual[key])}`;
10 | }
11 |
12 | return getDiffEntries().map(formatDiff).join(',\n');
13 | }
14 |
15 | function getMessage(pass, actual, expected, observed, verb, util, customEqualityTesters) {
16 | const passStrings = [
17 | `Expected ${actual} not to have been ${verb} with`,
18 | `${jasmine.pp(expected)}, but it was`
19 | ];
20 |
21 | const failStrings = [
22 | `Expected ${actual} to have been ${verb} with`,
23 | `${jasmine.pp(expected)}`,
24 | ...diffMessage()
25 | ];
26 |
27 | function diffMessage() {
28 | if (!observed.length) return [`but it was never ${verb}`];
29 |
30 | return [
31 | `${actual} was ${verb} with ${jasmine.pp(observed)}`,
32 | 'diff:',
33 | objectDiffs(observed[0], expected, util, customEqualityTesters)
34 | ];
35 | }
36 |
37 | return (pass ? passStrings : failStrings).join(',\n');
38 | }
39 |
40 | beforeEach(() => {
41 | jasmine.addMatchers({
42 | toHaveBeenDispatched(){
43 | return {
44 | compare(actual){
45 | const pass = Dispatcher.dispatch.calls.all().some((dispatchCall) => {
46 | return dispatchCall.args[0].type === actual;
47 | });
48 |
49 | const allDispatchers = Dispatcher.dispatch.calls.all().map((dispatchCall) => {
50 | return dispatchCall.args[0].type;
51 | });
52 |
53 | let message;
54 | if (pass) {
55 | message = `Expected ${actual} not to have been dispatched, but it was`;
56 | } else {
57 | message = `Expected ${actual} to have been dispatched, but it was not. \n Actual dispatch calls are ${allDispatchers.join(', ')}`;
58 | }
59 |
60 | return {pass, message};
61 | }
62 | };
63 | },
64 |
65 | toHaveBeenDispatchedWith(util, customEqualityTesters){
66 | return {
67 | compare(actual, expected){
68 | const observed = Dispatcher.dispatch.calls.all()
69 | .map(dispatchCall => dispatchCall.args[0])
70 | .filter(({type}) => type === actual);
71 |
72 | const pass = observed.some(params => {
73 | return util.equals(params, jasmine.objectContaining(expected), customEqualityTesters);
74 | });
75 |
76 | return {
77 | pass,
78 | message: getMessage(pass, actual, expected, observed, 'dispatched', util, customEqualityTesters)
79 | };
80 | }
81 | };
82 | }
83 | });
84 | });
--------------------------------------------------------------------------------
/spec/app/support/mock_router.js:
--------------------------------------------------------------------------------
1 | const Url = require('url');
2 | const Qs = require('qs');
3 |
4 | let routes = {};
5 |
6 | let OldRouter;
7 | let RouterContainer;
8 |
9 | const MockRouter = function() {
10 | return MockRouter;
11 | };
12 |
13 | function getParamsFromRoute(route, routeName) {
14 | const valueRegex = new RegExp('^' + routeName.replace(/\/:[^\/]*/g, '/([^\/]*)') + '$');
15 | const keys = (routeName.match(/:([^\/]*)/g) || []).map(key => key.replace(':', ''));
16 | let matches = route.match(valueRegex) || [];
17 | return {keys, matches};
18 | }
19 |
20 | function zip(keys, values) {
21 | return keys.reduce((memo, key, i) => {
22 | memo[key] = values[i];
23 | return memo;
24 | }, {});
25 | }
26 |
27 | Object.assign(MockRouter, {
28 | install(_RouterContainer) {
29 | RouterContainer = _RouterContainer;
30 | OldRouter = RouterContainer.Router;
31 | RouterContainer.Router = MockRouter;
32 | },
33 |
34 | uninstall() {
35 | RouterContainer.Router = OldRouter;
36 | },
37 |
38 | get(route, callback) {
39 | routes[route] = callback;
40 | },
41 |
42 | navigate: jasmine.createSpy('navigate').and.callFake(function(route) {
43 | const url = Url.parse(route);
44 | const queryParams = url.query ? Qs.parse(url.query) : {};
45 | const newRoute = Url.format({...url, query: null, search: null});
46 |
47 | const routedWithId = Object.keys(routes).find(function(routeName){
48 | let {keys, matches} = getParamsFromRoute(newRoute, routeName);
49 | if (matches.length > 1) {
50 | matches = matches.slice(1);
51 | const req = {params: {...queryParams, ...zip(keys, matches)}};
52 | routes[routeName](req);
53 | return true;
54 | }
55 | });
56 | if(!routedWithId) routes[newRoute]({params: {...queryParams}});
57 | })
58 | });
59 |
60 | module.exports = MockRouter;
61 |
--------------------------------------------------------------------------------
/spec/app/support/mock_router_spec.js:
--------------------------------------------------------------------------------
1 | require('../spec_helper');
2 |
3 | describe('MockRouter', () => {
4 | let route1Spy, route2Spy, router;
5 | beforeEach(() => {
6 | const {Router} = require('../../../app/components/use_router');
7 | router = new Router({pushState: true});
8 | route1Spy = jasmine.createSpy('route1');
9 | route2Spy = jasmine.createSpy('route2');
10 | router.get('foo', route1Spy);
11 | router.get('foo/:bar/:baz', route2Spy);
12 | });
13 |
14 | describe('#navigate', () => {
15 | it('works for a route without params', () => {
16 | router.navigate('foo');
17 | expect(route1Spy).toHaveBeenCalledWith({params: {}});
18 | });
19 |
20 | it('works for routes with params', () => {
21 | router.navigate('foo/abc/456');
22 | expect(route2Spy).toHaveBeenCalledWith({params: {bar: 'abc', baz: '456'}});
23 | });
24 |
25 | it('works with query params', () => {
26 | router.navigate('foo?bar=abc');
27 | expect(route1Spy).toHaveBeenCalledWith({params: {bar: 'abc'}});
28 | });
29 |
30 | it('works for routes with params with query params', () => {
31 | router.navigate('foo/abc/456?name=bob');
32 | expect(route2Spy).toHaveBeenCalledWith({params: {bar: 'abc', baz: '456', name: 'bob'}});
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/spec/factories/user.js:
--------------------------------------------------------------------------------
1 | const Factory = require('rosie').Factory;
2 |
3 | Factory.define('user')
4 | .sequence('name', id => `Bob ${id}`);
5 |
--------------------------------------------------------------------------------
/spec/integration/features_spec.js:
--------------------------------------------------------------------------------
1 | require('./spec_helper');
2 |
3 | describeWithWebdriver('Features', () => {
4 | let page;
5 |
6 | describe('when viewing the app', () => {
7 | beforeEach.async(async() => {
8 | page = (await visit('/')).page;
9 | await waitForExist(page, '.pui-react-starter');
10 | });
11 |
12 | it.async('can add a todoItem', async() => {
13 | await setValue(page, '.todo-adder input', 'DO THIS THING');
14 | await click(page, '.todo-adder button');
15 | await waitForText(page, '.todo-list .todo-item', 'DO THIS THING');
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/spec/integration/helpers/webdriver_helper.js:
--------------------------------------------------------------------------------
1 | const {compact} = require('../../../helpers/application_helper');
2 | const join = require('url-join');
3 | const JasmineWebdriver = require('../support/jasmine_webdriver');
4 |
5 | let webdriver;
6 |
7 | function visit(url) {
8 | return webdriver.driver().then(({driver}) => {
9 | return driver.url(join(...compact([`http://localhost:${process.env.PORT}`, url]))).then(() => ({page: driver}));
10 | });
11 | }
12 |
13 | function click(page, selector) {
14 | return waitForExist(page, selector)
15 | .then(function() {
16 | return page.click(selector);
17 | })
18 | .then(function(status) {
19 | if (!status) throw new Error(`click timed out waiting for ${selector}`);
20 | });
21 | }
22 |
23 | function waitForExist(page, selector) {
24 | return page.waitForExist(selector)
25 | .then(function(status) {
26 | if (!status) throw new Error(`WaitForExist timed out waiting for ${selector}`);
27 | });
28 | }
29 |
30 | function waitUntil(page, description, callback, timeout = 5000) {
31 | return page.waitUntil(callback, timeout)
32 | .then(function(status) {
33 | if (!status) throw new Error(`WaitUntil timed out waiting for ${description || callback.toString()}`);
34 | });
35 | }
36 |
37 | function waitUntilCondition(page, condition) {
38 | return waitUntil(page, `condition: ${condition.toString()}`, function() {
39 | return page.execute(condition).then(({value}) => value);
40 | });
41 | }
42 |
43 | function waitForCookie(page, cookie) {
44 | const codeToRunInPage = `function() { return document.cookie.match(new RegExp("${cookie}")); }`;
45 | /* eslint-disable no-eval */
46 | return waitUntilCondition(page, eval(`(function() { return ${codeToRunInPage}; })()`));
47 | /* eslint-enable no-eval */
48 | }
49 |
50 |
51 | function waitForCount(page, selector, count, operator = '===') {
52 | const codeToRunInPage = `function() { return document.querySelectorAll("${selector}").length ${operator} ${count} }`;
53 | /* eslint-disable no-eval */
54 | return waitUntilCondition(page, eval(`(function() { return ${codeToRunInPage}; })()`));
55 | /* eslint-enable no-eval */
56 | }
57 |
58 | function setValue(page, selector, inputText = '') {
59 | return waitForExist(page, selector)
60 | .then(function() {
61 | return page.setValue(selector, inputText);
62 | });
63 | }
64 |
65 | function waitForValue(page, selector, value, ...args) {
66 | return page.waitForValue(selector, value, ...args).then(function(status) {
67 | if(!status) throw new Error(`WaitForValue timed out waiting for ${selector}, ${value}`);
68 | });
69 | }
70 |
71 | function waitForText(page, selector, expectedText = '') {
72 | return waitForExist(page, selector)
73 | .then(function() {
74 | return page.waitUntil(() => page.getText(selector).then(text => text.includes(expectedText)));
75 | })
76 | .then(function(status) {
77 | if (!status) throw new Error(`WaitForText timed out waiting for ${selector}, ${expectedText}`);
78 | });
79 | }
80 |
81 | function sleep(time = 1000) {
82 | return new Promise(resolve => setTimeout(resolve, time));
83 | }
84 |
85 | function describeWithWebdriver(name, callback, options = {}) {
86 | describe(name, function() {
87 | beforeEach(() => {
88 | webdriver = webdriver || new JasmineWebdriver({timeout: 5000, ...options});
89 | });
90 |
91 | afterEach(async function(done) {
92 | await webdriver.end();
93 | done();
94 | });
95 |
96 | callback();
97 | });
98 | }
99 |
100 | module.exports = {click, describeWithWebdriver, visit, waitForCount, setValue, waitForValue, waitForText, waitForExist, waitForCookie, sleep};
101 |
--------------------------------------------------------------------------------
/spec/integration/spec_helper.js:
--------------------------------------------------------------------------------
1 | require('../support/bluebird');
2 | require('../spec_helper');
3 | const webdriverHelper = require('./helpers/webdriver_helper');
4 | const JasmineAsyncSuite = require('jasmine-async-suite');
5 |
6 | JasmineAsyncSuite.install();
7 |
8 | const {DEFAULT_TIMEOUT_INTERVAL} = jasmine;
9 |
10 | let globals = {...webdriverHelper};
11 | Object.assign(global, globals);
12 |
13 | beforeAll(() => {
14 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
15 | });
16 |
17 | afterAll(() => {
18 | jasmine.DEFAULT_TIMEOUT_INTERVAL = DEFAULT_TIMEOUT_INTERVAL;
19 | Object.keys(globals).forEach(key => delete global[key]);
20 | JasmineAsyncSuite.uninstall();
21 | });
--------------------------------------------------------------------------------
/spec/integration/support/jasmine_webdriver.js:
--------------------------------------------------------------------------------
1 | const selenium = require('./selenium');
2 | const thenify = require('thenify');
3 | const waitUntilListening = thenify(require('strong-wait-till-listening'));
4 | const webdriverio = require('webdriverio');
5 |
6 | const privates = new WeakMap();
7 |
8 | class JasmineWebdriver {
9 | constructor({browser = process.env.BROWSER || 'phantomjs', timeout = 500} = {}) {
10 | privates.set(this, {processes: [], desiredCapabilities: {browserName: browser}, timeout});
11 | }
12 |
13 | driver() {
14 | const {desiredCapabilities, processes, timeout} = privates.get(this);
15 | return new Promise(async (resolve, reject) => {
16 | const port = 4444;
17 | await selenium.install();
18 | const process = await selenium.start({spawnOptions: { stdio: [ 'ignore', 'ignore', 'ignore' ] }});
19 | processes.push({process, closed: new Promise(res => process.once('close', res))});
20 | await waitUntilListening({port, timeoutInMs: 30000})
21 | .catch(() => reject(`error in waiting for selenium server on port ${port}`));
22 | const driver = webdriverio.remote({desiredCapabilities, waitforTimeout: timeout}).init();
23 | await driver;
24 | processes.push({driver});
25 | resolve({driver});
26 | });
27 | }
28 |
29 | async end() {
30 | const {processes} = privates.get(this);
31 | const webdriverProcesses = processes.filter(p => p.driver).map(p => p.driver.end());
32 | await Promise.all(webdriverProcesses);
33 | const otherProcesses = processes.filter(p => p.process).map(p => (p.process.kill(), p.closed));
34 | await Promise.all(otherProcesses);
35 | return Promise.all([webdriverProcesses, ...otherProcesses]);
36 | }
37 | }
38 |
39 | module.exports = JasmineWebdriver;
--------------------------------------------------------------------------------
/spec/integration/support/selenium.js:
--------------------------------------------------------------------------------
1 | const seleniumStandalone = require('selenium-standalone');
2 | const thenify = require('thenify');
3 |
4 | module.exports = {
5 | install: thenify(seleniumStandalone.install),
6 | start: thenify(seleniumStandalone.start)
7 | };
8 |
--------------------------------------------------------------------------------
/spec/spec_helper.js:
--------------------------------------------------------------------------------
1 | require('babel-polyfill');
2 | const React = require('react');
3 | const {Factory} = require('rosie');
4 |
5 | let globals;
6 |
7 | beforeAll(() => {
8 | globals = {
9 | Factory,
10 | React
11 | };
12 | Object.assign(global, globals);
13 | });
14 |
15 | afterAll(() => {
16 | Object.keys(globals).forEach(key => delete global[key]);
17 | });
--------------------------------------------------------------------------------
/spec/support/bluebird.js:
--------------------------------------------------------------------------------
1 | const Bluebird = require('bluebird');
2 | Bluebird.prototype.catch = function(...args) {
3 | return Bluebird.prototype.then.call(this, i => i, ...args);
4 | };
5 | global.Promise = Bluebird;
6 |
--------------------------------------------------------------------------------
/spec/support/deferred.js:
--------------------------------------------------------------------------------
1 | const mockPromises = require('mock-promises');
2 |
3 | const Deferred = function() {
4 | let resolver, rejector;
5 | const promise = new Promise(function(res, rej) {
6 | resolver = res;
7 | rejector = rej;
8 | });
9 |
10 | const wrapper = Object.assign(promise, {
11 | resolve(...args) {
12 | resolver(...args);
13 | mockPromises.executeForPromise(promise);
14 | return wrapper;
15 | },
16 | reject(...args) {
17 | rejector(...args);
18 | mockPromises.executeForPromise(promise);
19 | return wrapper;
20 | },
21 | promise() {
22 | return promise;
23 | }
24 | });
25 | return wrapper;
26 | };
27 |
28 | module.exports = Deferred;
--------------------------------------------------------------------------------
/spec/support/mock_fetch.js:
--------------------------------------------------------------------------------
1 | let isomorphicFetch;
2 | const nativeFetch = global.fetch;
3 |
4 | module.exports = {
5 | install() {
6 | if (!isomorphicFetch) {
7 | global.fetch = null;
8 | isomorphicFetch = require('isomorphic-fetch');
9 | }
10 | global.fetch = isomorphicFetch;
11 | },
12 |
13 | uninstall() {
14 | global.fetch = nativeFetch;
15 | }
16 | };
--------------------------------------------------------------------------------
/tasks/default.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const runSequence = require('run-sequence');
3 |
4 | gulp.task('default', cb => runSequence('lint', 'spec-app', 'spec-integration', cb));
5 |
--------------------------------------------------------------------------------
/tasks/deploy.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const runSequence = require('run-sequence');
3 | const {spawn} = require('child_process');
4 |
5 | gulp.task('push', (callback) => {
6 | spawn('cf', ['push'], {stdio: 'inherit', env: process.env}).once('close', callback);
7 | });
8 |
9 | gulp.task('copy-staticfile', () => {
10 | return gulp.src('Staticfile').pipe(gulp.dest('public'));
11 | });
12 |
13 | gulp.task('deploy', (done) => {
14 | const {NODE_ENV: env} = process.env;
15 | process.env.NODE_ENV = 'production';
16 | runSequence('clean-assets', 'assets', 'assets-config', 'copy-staticfile', 'push', () => {
17 | process.env.NODE_ENV = env;
18 | done();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/tasks/dev_server.js:
--------------------------------------------------------------------------------
1 | import WebpackDevServer from 'webpack-dev-server';
2 | import gulp from 'gulp';
3 | import webpack from 'webpack';
4 |
5 | const devServerPort = 3000;
6 |
7 | let server;
8 | function kill() {
9 | if (server) server.close();
10 | }
11 |
12 | gulp.task('dev-server', done => {
13 | const config = require('../config/webpack/development.js')();
14 | const compiler = webpack(config);
15 | compiler.plugin('done', () => {
16 | done();
17 | });
18 | server = new WebpackDevServer(compiler, config.devServer);
19 |
20 | const port = process.env.PORT || devServerPort;
21 | /* eslint-disable no-console */
22 | console.log(`dev server listening on port ${port}`);
23 | /* eslint-enable no-console */
24 | server.listen(port);
25 | });
26 |
27 | export {kill};
--------------------------------------------------------------------------------
/tasks/integration.js:
--------------------------------------------------------------------------------
1 | import portastic from 'portastic';
2 |
3 | const {compact} = require('../helpers/application_helper');
4 | const gulp = require('gulp');
5 | const plugins = require('gulp-load-plugins')();
6 | const runSequence = require('run-sequence');
7 | const {killServer} = require('../tasks/server');
8 | const {kill: killDevServer} = require('../tasks/dev_server');
9 | const assetPort = 3000;
10 |
11 | function buildSequence(isForemanNotRunning, env) {
12 | const {ASSET_PORT, NODE_ENV, PORT} = process.env;
13 | return {
14 | sequence: compact([
15 | isForemanNotRunning && 'server',
16 | isForemanNotRunning && 'dev-server',
17 | 'wait-for-server',
18 | 'jasmine-integration'
19 | ]),
20 | cleanup: done => () => {
21 | Object.assign(process.env, {NODE_ENV, ASSET_PORT, PORT});
22 | if(isForemanNotRunning) {
23 | killDevServer();
24 | killServer();
25 | }
26 |
27 | done();
28 | },
29 | env
30 | };
31 | }
32 |
33 | function runIntegration() {
34 | return Promise.all([
35 | portastic.test(assetPort),
36 | portastic.find({min: 8000, max: 8080, retrieve: 2}),
37 | ]).then(([isForemanNotRunning, openPorts]) => {
38 | const port = isForemanNotRunning ? openPorts[0] : assetPort;
39 | const apiPort = isForemanNotRunning ? openPorts[1] : 3001;
40 | let environment = {
41 | NODE_ENV: 'integration',
42 | PORT: port,
43 | API_PORT: apiPort
44 | };
45 |
46 | const {env, sequence, cleanup} = buildSequence(isForemanNotRunning, environment);
47 | Object.assign(process.env, env);
48 | return {sequence, cleanup};
49 | });
50 | }
51 |
52 | gulp.task('jasmine-integration', () => {
53 | return gulp.src('spec/integration/**/*_spec.js')
54 | .pipe(plugins.plumber())
55 | .pipe(plugins.jasmine({includeStackTrace: true}));
56 | });
57 |
58 | gulp.task('spec-integration', done => {
59 | runIntegration().then(({sequence, cleanup}) => runSequence(...sequence, cleanup(done)));
60 | });
61 |
--------------------------------------------------------------------------------
/tasks/react_tools.js:
--------------------------------------------------------------------------------
1 | import {Assets, Foreman, Jasmine, Lint} from 'pui-react-tools';
2 | import test from '../config/webpack/test';
3 | import development from '../config/webpack/development';
4 | import production from '../config/webpack/production';
5 |
6 | Assets.install({
7 | webpack: {
8 | development,
9 | production,
10 | integration: production
11 | }
12 | });
13 |
14 | Foreman.install();
15 | Lint.install();
16 |
17 | Jasmine.install({
18 | webpack: {test}
19 | });
20 |
--------------------------------------------------------------------------------
/tasks/server.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const plugins = require('gulp-load-plugins')();
3 | const {spawn} = require('child_process');
4 | const waitUntilListening = require('strong-wait-till-listening');
5 |
6 | let node;
7 | function restartServer() {
8 | if (node) return node.kill('SIGUSR2');
9 | }
10 | function killServer() {
11 | if (node) return node.kill();
12 | }
13 |
14 | process.on('exit', restartServer);
15 |
16 | gulp.task('server', function() {
17 | if (node) return node.kill('SIGUSR2');
18 | node = spawn('node', ['index.js'], {stdio: 'inherit', env: process.env});
19 | node.on('close', function(code) {
20 | if (code === 8) {
21 | node = null;
22 | plugins.util.log('Error detected, waiting for changes...');
23 | }
24 | });
25 | });
26 |
27 | gulp.task('wait-for-server', function(callback) {
28 | /* eslint-disable no-console */
29 | console.log(`waiting for server on ${process.env.API_PORT}`);
30 | /* eslint-enable no-console */
31 | waitUntilListening({port: process.env.API_PORT, timeoutInMs: 90000}, callback);
32 | });
33 |
34 | gulp.task('watch-server', function() {
35 | gulp.watch(['server/**/*.js', 'helpers/**/*.js', 'lib/**/*.js', 'config/*.json'], ['server']);
36 | });
37 |
38 | gulp.task('s', ['server', 'watch-server']);
39 |
40 | module.exports = {restartServer, killServer};
41 |
--------------------------------------------------------------------------------
/tmp/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vmware-archive/react-starter/9cab452fe622c02f4b8c34eaffc3d0e82cb4d132/tmp/.gitkeep
--------------------------------------------------------------------------------