tag', () => {
8 | const renderedComponent = shallow( );
9 | expect(renderedComponent.type()).toEqual('button');
10 | });
11 |
12 | it('should have a className attribute', () => {
13 | const renderedComponent = shallow( );
14 | expect(renderedComponent.prop('className')).toBeDefined();
15 | });
16 |
17 | it('should adopt a valid attribute', () => {
18 | const id = 'test';
19 | const renderedComponent = shallow( );
20 | expect(renderedComponent.prop('id')).toEqual(id);
21 | });
22 |
23 | it('should not adopt an invalid attribute', () => {
24 | const renderedComponent = shallow( );
25 | expect(renderedComponent.prop('attribute')).toBeUndefined();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/app/containers/FeaturePage/tests/ListItemTitle.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import ListItemTitle from '../ListItemTitle';
5 |
6 | describe(' ', () => {
7 | it('should render an tag', () => {
8 | const renderedComponent = shallow( );
9 | expect(renderedComponent.type()).toEqual('p');
10 | });
11 |
12 | it('should have a className attribute', () => {
13 | const renderedComponent = shallow( );
14 | expect(renderedComponent.prop('className')).toBeDefined();
15 | });
16 |
17 | it('should adopt a valid attribute', () => {
18 | const id = 'test';
19 | const renderedComponent = shallow( );
20 | expect(renderedComponent.prop('id')).toEqual(id);
21 | });
22 |
23 | it('should not adopt an invalid attribute', () => {
24 | const renderedComponent = shallow( );
25 | expect(renderedComponent.prop('attribute')).toBeUndefined();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/app/utils/tests/checkStore.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test injectors
3 | */
4 |
5 | import checkStore from '../checkStore';
6 |
7 | describe('checkStore', () => {
8 | let store;
9 |
10 | beforeEach(() => {
11 | store = {
12 | dispatch: () => {},
13 | subscribe: () => {},
14 | getState: () => {},
15 | replaceReducer: () => {},
16 | runSaga: () => {},
17 | injectedReducers: {},
18 | injectedSagas: {},
19 | };
20 | });
21 |
22 | it('should not throw if passed valid store shape', () => {
23 | expect(() => checkStore(store)).not.toThrow();
24 | });
25 |
26 | it('should throw if passed invalid store shape', () => {
27 | expect(() => checkStore({})).toThrow();
28 | expect(() => checkStore({ ...store, injectedSagas: null })).toThrow();
29 | expect(() => checkStore({ ...store, injectedReducers: null })).toThrow();
30 | expect(() => checkStore({ ...store, runSaga: null })).toThrow();
31 | expect(() => checkStore({ ...store, replaceReducer: null })).toThrow();
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/app/components/Header/tests/A.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import renderer from 'react-test-renderer';
4 | import 'jest-styled-components';
5 |
6 | import A from '../A';
7 |
8 | describe(' ', () => {
9 | it('should match the snapshot', () => {
10 | const renderedComponent = renderer.create( ).toJSON();
11 | expect(renderedComponent).toMatchSnapshot();
12 | });
13 |
14 | it('should have a className attribute', () => {
15 | const renderedComponent = mount( );
16 | expect(renderedComponent.find('a').prop('className')).toBeDefined();
17 | });
18 |
19 | it('should adopt a valid attribute', () => {
20 | const id = 'test';
21 | const renderedComponent = mount( );
22 | expect(renderedComponent.find('a').prop('id')).toEqual(id);
23 | });
24 |
25 | it('should not adopt an invalid attribute', () => {
26 | const renderedComponent = mount( );
27 | expect(renderedComponent.find('a').prop('attribute')).toBeUndefined();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/app/containers/HomePage/tests/CenteredSection.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import CenteredSection from '../CenteredSection';
5 |
6 | describe(' ', () => {
7 | it('should render an tag', () => {
8 | const renderedComponent = shallow( );
9 | expect(renderedComponent.type()).toEqual('section');
10 | });
11 |
12 | it('should have a className attribute', () => {
13 | const renderedComponent = shallow( );
14 | expect(renderedComponent.prop('className')).toBeDefined();
15 | });
16 |
17 | it('should adopt a valid attribute', () => {
18 | const id = 'test';
19 | const renderedComponent = shallow( );
20 | expect(renderedComponent.prop('id')).toEqual(id);
21 | });
22 |
23 | it('should not adopt an invalid attribute', () => {
24 | const renderedComponent = shallow( );
25 | expect(renderedComponent.prop('attribute')).toBeUndefined();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/internals/templates/utils/tests/checkStore.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test injectors
3 | */
4 |
5 | import checkStore from '../checkStore';
6 |
7 | describe('checkStore', () => {
8 | let store;
9 |
10 | beforeEach(() => {
11 | store = {
12 | dispatch: () => {},
13 | subscribe: () => {},
14 | getState: () => {},
15 | replaceReducer: () => {},
16 | runSaga: () => {},
17 | injectedReducers: {},
18 | injectedSagas: {},
19 | };
20 | });
21 |
22 | it('should not throw if passed valid store shape', () => {
23 | expect(() => checkStore(store)).not.toThrow();
24 | });
25 |
26 | it('should throw if passed invalid store shape', () => {
27 | expect(() => checkStore({})).toThrow();
28 | expect(() => checkStore({ ...store, injectedSagas: null })).toThrow();
29 | expect(() => checkStore({ ...store, injectedReducers: null })).toThrow();
30 | expect(() => checkStore({ ...store, runSaga: null })).toThrow();
31 | expect(() => checkStore({ ...store, replaceReducer: null })).toThrow();
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/app/containers/App/selectors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The global state selectors
3 | */
4 |
5 | import { createSelector } from 'reselect';
6 |
7 | const selectGlobal = state => state.get('global');
8 |
9 | const selectRoute = state => state.get('route');
10 |
11 | const makeSelectCurrentUser = () =>
12 | createSelector(selectGlobal, globalState => globalState.get('currentUser'));
13 |
14 | const makeSelectLoading = () =>
15 | createSelector(selectGlobal, globalState => globalState.get('loading'));
16 |
17 | const makeSelectError = () =>
18 | createSelector(selectGlobal, globalState => globalState.get('error'));
19 |
20 | const makeSelectRepos = () =>
21 | createSelector(selectGlobal, globalState =>
22 | globalState.getIn(['userData', 'repositories']),
23 | );
24 |
25 | const makeSelectLocation = () =>
26 | createSelector(selectRoute, routeState => routeState.get('location').toJS());
27 |
28 | export {
29 | selectGlobal,
30 | makeSelectCurrentUser,
31 | makeSelectLoading,
32 | makeSelectError,
33 | makeSelectRepos,
34 | makeSelectLocation,
35 | };
36 |
--------------------------------------------------------------------------------
/app/containers/HomePage/messages.js:
--------------------------------------------------------------------------------
1 | /*
2 | * HomePage Messages
3 | *
4 | * This contains all the text for the HomePage component.
5 | */
6 | import { defineMessages } from 'react-intl';
7 |
8 | export default defineMessages({
9 | startProjectHeader: {
10 | id: 'boilerplate.containers.HomePage.start_project.header',
11 | defaultMessage: 'Start your next react project in seconds',
12 | },
13 | startProjectMessage: {
14 | id: 'boilerplate.containers.HomePage.start_project.message',
15 | defaultMessage:
16 | 'A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices',
17 | },
18 | trymeHeader: {
19 | id: 'boilerplate.containers.HomePage.tryme.header',
20 | defaultMessage: 'Hello Reactron',
21 | },
22 | trymeMessage: {
23 | id: 'boilerplate.containers.HomePage.tryme.message',
24 | defaultMessage: 'Show Github repositories by',
25 | },
26 | trymeAtPrefix: {
27 | id: 'boilerplate.containers.HomePage.tryme.atPrefix',
28 | defaultMessage: '@',
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/internals/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | React.js Boilerplate
10 |
11 |
12 |
13 | If you're seeing this message, that means JavaScript has been disabled on your browser , please enable JS to make this app work.
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/containers/RepoListItem/tests/IssueIcon.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import renderer from 'react-test-renderer';
4 | import 'jest-styled-components';
5 |
6 | import IssueIcon from '../IssueIcon';
7 |
8 | describe(' ', () => {
9 | it('should match the snapshot', () => {
10 | const renderedComponent = renderer.create( ).toJSON();
11 | expect(renderedComponent).toMatchSnapshot();
12 | });
13 |
14 | it('should have a className attribute', () => {
15 | const renderedComponent = shallow( );
16 | expect(renderedComponent.prop('className')).toBeDefined();
17 | });
18 |
19 | it('should adopt a valid attribute', () => {
20 | const id = 'test';
21 | const renderedComponent = shallow( );
22 | expect(renderedComponent.prop('id')).toEqual(id);
23 | });
24 |
25 | it('should adopt any attribute', () => {
26 | const renderedComponent = shallow( );
27 | expect(renderedComponent.prop('attribute')).toBeDefined();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/app/containers/RepoListItem/tests/RepoLink.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import renderer from 'react-test-renderer';
4 | import 'jest-styled-components';
5 |
6 | import RepoLink from '../RepoLink';
7 |
8 | describe(' ', () => {
9 | it('should match the snapshot', () => {
10 | const renderedComponent = renderer.create( ).toJSON();
11 | expect(renderedComponent).toMatchSnapshot();
12 | });
13 |
14 | it('should have a className attribute', () => {
15 | const renderedComponent = shallow( );
16 | expect(renderedComponent.prop('className')).toBeDefined();
17 | });
18 |
19 | it('should adopt a valid attribute', () => {
20 | const id = 'test';
21 | const renderedComponent = shallow( );
22 | expect(renderedComponent.prop('id')).toEqual(id);
23 | });
24 |
25 | it('should not adopt an invalid attribute', () => {
26 | const renderedComponent = shallow( );
27 | expect(renderedComponent.prop('attribute')).toBeUndefined();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/app/components/Footer/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { FormattedMessage } from 'react-intl';
4 |
5 | import A from 'components/A';
6 | import messages from '../messages';
7 | import Footer from '../index';
8 |
9 | describe('', () => {
10 | it('should render the copyright notice', () => {
11 | const renderedComponent = shallow();
12 | expect(
13 | renderedComponent.contains(
14 | ,
17 | ),
18 | ).toBe(true);
19 | });
20 |
21 | it('should render the credits', () => {
22 | const renderedComponent = shallow();
23 | expect(
24 | renderedComponent.contains(
25 |
26 | Max Stoiber,
30 | }}
31 | />
32 | ,
33 | ),
34 | ).toBe(true);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/app/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormattedMessage } from 'react-intl';
3 |
4 | import A from './A';
5 | import Img from './Img';
6 | import NavBar from './NavBar';
7 | import HeaderLink from './HeaderLink';
8 | import Banner from './banner.jpg';
9 | import messages from './messages';
10 |
11 | /* eslint-disable react/prefer-stateless-function */
12 | class Header extends React.Component {
13 | render() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | export default Header;
36 |
--------------------------------------------------------------------------------
/app/containers/RepoListItem/tests/IssueLink.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import renderer from 'react-test-renderer';
4 | import 'jest-styled-components';
5 |
6 | import IssueLink from '../IssueLink';
7 |
8 | describe(' ', () => {
9 | it('should match the snapshot', () => {
10 | const renderedComponent = renderer.create( ).toJSON();
11 | expect(renderedComponent).toMatchSnapshot();
12 | });
13 |
14 | it('should have a className attribute', () => {
15 | const renderedComponent = shallow( );
16 | expect(renderedComponent.prop('className')).toBeDefined();
17 | });
18 |
19 | it('should adopt a valid attribute', () => {
20 | const id = 'test';
21 | const renderedComponent = shallow( );
22 | expect(renderedComponent.prop('id')).toEqual(id);
23 | });
24 |
25 | it('should not adopt an invalid attribute', () => {
26 | const renderedComponent = shallow( );
27 | expect(renderedComponent.prop('attribute')).toBeUndefined();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/app/components/Button/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Button.js
4 | *
5 | * A common button, if you pass it a prop "route" it'll render a link to a react-router route
6 | * otherwise it'll render a link with an onclick
7 | */
8 |
9 | import React, { Children } from 'react';
10 | import PropTypes from 'prop-types';
11 |
12 | import A from './A';
13 | import StyledButton from './StyledButton';
14 | import Wrapper from './Wrapper';
15 |
16 | function Button(props) {
17 | // Render an anchor tag
18 | let button = (
19 |
20 | {Children.toArray(props.children)}
21 |
22 | );
23 |
24 | // If the Button has a handleRoute prop, we want to render a button
25 | if (props.handleRoute) {
26 | button = (
27 |
28 | {Children.toArray(props.children)}
29 |
30 | );
31 | }
32 |
33 | return {button} ;
34 | }
35 |
36 | Button.propTypes = {
37 | handleRoute: PropTypes.func,
38 | href: PropTypes.string,
39 | onClick: PropTypes.func,
40 | children: PropTypes.node.isRequired,
41 | };
42 |
43 | export default Button;
44 |
--------------------------------------------------------------------------------
/app/utils/injectReducer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import hoistNonReactStatics from 'hoist-non-react-statics';
4 |
5 | import getInjectors from './reducerInjectors';
6 |
7 | /**
8 | * Dynamically injects a reducer
9 | *
10 | * @param {string} key A key of the reducer
11 | * @param {function} reducer A reducer that will be injected
12 | *
13 | */
14 | export default ({ key, reducer }) => WrappedComponent => {
15 | class ReducerInjector extends React.Component {
16 | static WrappedComponent = WrappedComponent;
17 | static contextTypes = {
18 | store: PropTypes.object.isRequired,
19 | };
20 | static displayName = `withReducer(${WrappedComponent.displayName ||
21 | WrappedComponent.name ||
22 | 'Component'})`;
23 |
24 | componentWillMount() {
25 | const { injectReducer } = this.injectors;
26 |
27 | injectReducer(key, reducer);
28 | }
29 |
30 | injectors = getInjectors(this.context.store);
31 |
32 | render() {
33 | return ;
34 | }
35 | }
36 |
37 | return hoistNonReactStatics(ReducerInjector, WrappedComponent);
38 | };
39 |
--------------------------------------------------------------------------------
/internals/templates/utils/injectReducer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import hoistNonReactStatics from 'hoist-non-react-statics';
4 |
5 | import getInjectors from './reducerInjectors';
6 |
7 | /**
8 | * Dynamically injects a reducer
9 | *
10 | * @param {string} key A key of the reducer
11 | * @param {function} reducer A reducer that will be injected
12 | *
13 | */
14 | export default ({ key, reducer }) => WrappedComponent => {
15 | class ReducerInjector extends React.Component {
16 | static WrappedComponent = WrappedComponent;
17 | static contextTypes = {
18 | store: PropTypes.object.isRequired,
19 | };
20 | static displayName = `withReducer(${WrappedComponent.displayName ||
21 | WrappedComponent.name ||
22 | 'Component'})`;
23 |
24 | componentWillMount() {
25 | const { injectReducer } = this.injectors;
26 |
27 | injectReducer(key, reducer);
28 | }
29 |
30 | injectors = getInjectors(this.context.store);
31 |
32 | render() {
33 | return ;
34 | }
35 | }
36 |
37 | return hoistNonReactStatics(ReducerInjector, WrappedComponent);
38 | };
39 |
--------------------------------------------------------------------------------
/app/components/List/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import ListItem from 'components/ListItem';
5 | import List from '../index';
6 |
7 | describe('
', () => {
8 | it('should render the component if no items are passed', () => {
9 | const renderedComponent = shallow(
);
10 | expect(renderedComponent.find(ListItem)).toBeDefined();
11 | });
12 |
13 | it('should pass all items props to rendered component', () => {
14 | const items = [{ id: 1, name: 'Hello' }, { id: 2, name: 'World' }];
15 |
16 | const component = ({ item }) => {item.name} ; // eslint-disable-line react/prop-types
17 |
18 | const renderedComponent = shallow(
19 |
,
20 | );
21 | expect(renderedComponent.find(component)).toHaveLength(2);
22 | expect(
23 | renderedComponent
24 | .find(component)
25 | .at(0)
26 | .prop('item'),
27 | ).toBe(items[0]);
28 | expect(
29 | renderedComponent
30 | .find(component)
31 | .at(1)
32 | .prop('item'),
33 | ).toBe(items[1]);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactron",
3 | "productName": "Reactron",
4 | "version": "1.0.8",
5 | "description": "Reactron Desktop App",
6 | "license": "Apache-2.0",
7 | "copyright": "Manish Jangir",
8 | "author": {
9 | "name": "Manish Jangir",
10 | "email": "mjangir70@gmail.com"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/mjangir/reactron.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/mjangir/reactron/issues"
18 | },
19 | "main": "./electron/main.prod.js",
20 | "keywords": [
21 | "Reactron",
22 | "react with electron",
23 | "electron",
24 | "electron boilerplate",
25 | "react electron"
26 | ],
27 | "scripts": {
28 | "electron-rebuild": "cross-env BABEL_ENV=electron node -r babel-register ../internals/scripts/electron-rebuild.js",
29 | "postinstall": "npm run electron-rebuild"
30 | },
31 | "dependencies": {
32 | "@sentry/electron": "^0.9.0",
33 | "auto-launch": "^5.0.5",
34 | "electron-is-dev": "^0.3.0",
35 | "electron-log": "^2.2.17",
36 | "electron-settings": "^3.2.0",
37 | "electron-updater": "^3.1.2",
38 | "electron-window-state": "^5.0.1"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/components/ToggleOption/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow, mount } from 'enzyme';
3 | import { IntlProvider, defineMessages } from 'react-intl';
4 |
5 | import ToggleOption from '../index';
6 |
7 | describe(' ', () => {
8 | it('should render default language messages', () => {
9 | const defaultEnMessage = 'someContent';
10 | const message = defineMessages({
11 | enMessage: {
12 | id: 'boilerplate.containers.LocaleToggle.en',
13 | defaultMessage: defaultEnMessage,
14 | },
15 | });
16 | const renderedComponent = shallow(
17 |
18 |
19 | ,
20 | );
21 | expect(
22 | renderedComponent.contains(
23 | ,
24 | ),
25 | ).toBe(true);
26 | });
27 |
28 | it('should display `value`(two letter language code) when `message` is absent', () => {
29 | const renderedComponent = mount(
30 |
31 |
32 | ,
33 | );
34 | expect(renderedComponent.text()).toBe('de');
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/app/utils/reducerInjectors.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant';
2 | import isEmpty from 'lodash/isEmpty';
3 | import isFunction from 'lodash/isFunction';
4 | import isString from 'lodash/isString';
5 |
6 | import checkStore from './checkStore';
7 | import createReducer from '../reducers';
8 |
9 | export function injectReducerFactory(store, isValid) {
10 | return function injectReducer(key, reducer) {
11 | if (!isValid) checkStore(store);
12 |
13 | invariant(
14 | isString(key) && !isEmpty(key) && isFunction(reducer),
15 | '(app/utils...) injectReducer: Expected `reducer` to be a reducer function',
16 | );
17 |
18 | // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
19 | if (
20 | Reflect.has(store.injectedReducers, key) &&
21 | store.injectedReducers[key] === reducer
22 | )
23 | return;
24 |
25 | store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
26 | store.replaceReducer(createReducer(store.injectedReducers));
27 | };
28 | }
29 |
30 | export default function getInjectors(store) {
31 | checkStore(store);
32 |
33 | return {
34 | injectReducer: injectReducerFactory(store, true),
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/internals/templates/utils/reducerInjectors.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant';
2 | import isEmpty from 'lodash/isEmpty';
3 | import isFunction from 'lodash/isFunction';
4 | import isString from 'lodash/isString';
5 |
6 | import checkStore from './checkStore';
7 | import createReducer from '../reducers';
8 |
9 | export function injectReducerFactory(store, isValid) {
10 | return function injectReducer(key, reducer) {
11 | if (!isValid) checkStore(store);
12 |
13 | invariant(
14 | isString(key) && !isEmpty(key) && isFunction(reducer),
15 | '(app/utils...) injectReducer: Expected `reducer` to be a reducer function',
16 | );
17 |
18 | // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
19 | if (
20 | Reflect.has(store.injectedReducers, key) &&
21 | store.injectedReducers[key] === reducer
22 | )
23 | return;
24 |
25 | store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
26 | store.replaceReducer(createReducer(store.injectedReducers));
27 | };
28 | }
29 |
30 | export default function getInjectors(store) {
31 | checkStore(store);
32 |
33 | return {
34 | injectReducer: injectReducerFactory(store, true),
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/app/containers/LanguageProvider/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * LanguageProvider
4 | *
5 | * this component connects the redux state language locale to the
6 | * IntlProvider component and i18n messages (loaded from `app/translations`)
7 | */
8 |
9 | import React from 'react';
10 | import PropTypes from 'prop-types';
11 | import { connect } from 'react-redux';
12 | import { createSelector } from 'reselect';
13 | import { IntlProvider } from 'react-intl';
14 |
15 | import { makeSelectLocale } from './selectors';
16 |
17 | export class LanguageProvider extends React.PureComponent {
18 | // eslint-disable-line react/prefer-stateless-function
19 | render() {
20 | return (
21 |
26 | {React.Children.only(this.props.children)}
27 |
28 | );
29 | }
30 | }
31 |
32 | LanguageProvider.propTypes = {
33 | locale: PropTypes.string,
34 | messages: PropTypes.object,
35 | children: PropTypes.element.isRequired,
36 | };
37 |
38 | const mapStateToProps = createSelector(makeSelectLocale(), locale => ({
39 | locale,
40 | }));
41 |
42 | export default connect(mapStateToProps)(LanguageProvider);
43 |
--------------------------------------------------------------------------------
/app/utils/electronSystemUtil.js:
--------------------------------------------------------------------------------
1 | import { remote } from 'electron';
2 | import os from 'os';
3 |
4 | const { app } = remote;
5 |
6 | let instance = null;
7 |
8 | class SystemUtil {
9 | constructor() {
10 | if (instance) {
11 | return instance;
12 | }
13 | instance = this;
14 |
15 | this.connectivityERR = [
16 | 'ERR_INTERNET_DISCONNECTED',
17 | 'ERR_PROXY_CONNECTION_FAILED',
18 | 'ERR_CONNECTION_RESET',
19 | 'ERR_NOT_CONNECTED',
20 | 'ERR_NAME_NOT_RESOLVED',
21 | 'ERR_NETWORK_CHANGED',
22 | ];
23 | this.userAgent = null;
24 |
25 | return instance;
26 | }
27 |
28 | getOS() {
29 | if (os.platform() === 'darwin') {
30 | return 'Mac';
31 | }
32 | if (os.platform() === 'linux') {
33 | return 'Linux';
34 | }
35 | if (os.platform() === 'win32' || os.platform() === 'win64') {
36 | if (parseFloat(os.release()) < 6.2) {
37 | return 'Windows 7';
38 | }
39 | return 'Windows 10';
40 | }
41 | return null;
42 | }
43 |
44 | setUserAgent(webViewUserAgent) {
45 | this.userAgent = `Reactron/${app.getVersion()} ${webViewUserAgent}`;
46 | }
47 |
48 | getUserAgent() {
49 | return this.userAgent;
50 | }
51 | }
52 |
53 | export default new SystemUtil();
54 |
--------------------------------------------------------------------------------
/app/components/Header/tests/Img.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import renderer from 'react-test-renderer';
4 | import 'jest-styled-components';
5 |
6 | import Img from '../Img';
7 |
8 | describe(' ', () => {
9 | it('should match the snapshot', () => {
10 | const renderedComponent = renderer
11 | .create( )
12 | .toJSON();
13 | expect(renderedComponent).toMatchSnapshot();
14 | });
15 |
16 | it('should have a className attribute', () => {
17 | const renderedComponent = mount(
18 | ,
19 | );
20 | expect(renderedComponent.find('img').prop('className')).toBeDefined();
21 | });
22 |
23 | it('should adopt a valid attribute', () => {
24 | const renderedComponent = mount(
25 | ,
26 | );
27 | expect(renderedComponent.find('img').prop('alt')).toEqual('test');
28 | });
29 |
30 | it('should not adopt an invalid attribute', () => {
31 | const renderedComponent = mount(
32 | ,
33 | );
34 | expect(renderedComponent.find('img').prop('attribute')).toBeUndefined();
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/app/tests/store.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test store addons
3 | */
4 |
5 | import { browserHistory } from 'react-router-dom';
6 | import configureStore from '../configureStore';
7 |
8 | describe('configureStore', () => {
9 | let store;
10 |
11 | beforeAll(() => {
12 | store = configureStore({}, browserHistory);
13 | });
14 |
15 | describe('injectedReducers', () => {
16 | it('should contain an object for reducers', () => {
17 | expect(typeof store.injectedReducers).toBe('object');
18 | });
19 | });
20 |
21 | describe('injectedSagas', () => {
22 | it('should contain an object for sagas', () => {
23 | expect(typeof store.injectedSagas).toBe('object');
24 | });
25 | });
26 |
27 | describe('runSaga', () => {
28 | it('should contain a hook for `sagaMiddleware.run`', () => {
29 | expect(typeof store.runSaga).toBe('function');
30 | });
31 | });
32 | });
33 |
34 | describe('configureStore params', () => {
35 | it('should call window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__', () => {
36 | /* eslint-disable no-underscore-dangle */
37 | const compose = jest.fn();
38 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = () => compose;
39 | configureStore(undefined, browserHistory);
40 | expect(compose).toHaveBeenCalled();
41 | /* eslint-enable */
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/app/containers/App/tests/actions.test.js:
--------------------------------------------------------------------------------
1 | import { LOAD_REPOS, LOAD_REPOS_SUCCESS, LOAD_REPOS_ERROR } from '../constants';
2 |
3 | import { loadRepos, reposLoaded, repoLoadingError } from '../actions';
4 |
5 | describe('App Actions', () => {
6 | describe('loadRepos', () => {
7 | it('should return the correct type', () => {
8 | const expectedResult = {
9 | type: LOAD_REPOS,
10 | };
11 |
12 | expect(loadRepos()).toEqual(expectedResult);
13 | });
14 | });
15 |
16 | describe('reposLoaded', () => {
17 | it('should return the correct type and the passed repos', () => {
18 | const fixture = ['Test'];
19 | const username = 'test';
20 | const expectedResult = {
21 | type: LOAD_REPOS_SUCCESS,
22 | repos: fixture,
23 | username,
24 | };
25 |
26 | expect(reposLoaded(fixture, username)).toEqual(expectedResult);
27 | });
28 | });
29 |
30 | describe('repoLoadingError', () => {
31 | it('should return the correct type and the error', () => {
32 | const fixture = {
33 | msg: 'Something went wrong!',
34 | };
35 | const expectedResult = {
36 | type: LOAD_REPOS_ERROR,
37 | error: fixture,
38 | };
39 |
40 | expect(repoLoadingError(fixture)).toEqual(expectedResult);
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/app/components/LoadingIndicator/Circle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled, { keyframes } from 'styled-components';
4 |
5 | const circleFadeDelay = keyframes`
6 | 0%,
7 | 39%,
8 | 100% {
9 | opacity: 0;
10 | }
11 |
12 | 40% {
13 | opacity: 1;
14 | }
15 | `;
16 |
17 | const Circle = props => {
18 | const CirclePrimitive = styled.div`
19 | width: 100%;
20 | height: 100%;
21 | position: absolute;
22 | left: 0;
23 | top: 0;
24 | ${props.rotate &&
25 | `
26 | -webkit-transform: rotate(${props.rotate}deg);
27 | -ms-transform: rotate(${props.rotate}deg);
28 | transform: rotate(${props.rotate}deg);
29 | `} &:before {
30 | content: '';
31 | display: block;
32 | margin: 0 auto;
33 | width: 15%;
34 | height: 15%;
35 | background-color: #999;
36 | border-radius: 100%;
37 | animation: ${circleFadeDelay} 1.2s infinite ease-in-out both;
38 | ${props.delay &&
39 | `
40 | -webkit-animation-delay: ${props.delay}s;
41 | animation-delay: ${props.delay}s;
42 | `};
43 | }
44 | `;
45 | return ;
46 | };
47 |
48 | Circle.propTypes = {
49 | delay: PropTypes.number,
50 | rotate: PropTypes.number,
51 | };
52 |
53 | export default Circle;
54 |
--------------------------------------------------------------------------------
/internals/templates/reducers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Combine all reducers in this file and export the combined reducers.
3 | */
4 |
5 | import { combineReducers } from 'redux-immutable';
6 | import { fromJS } from 'immutable';
7 | import { LOCATION_CHANGE } from 'react-router-redux';
8 |
9 | import languageProviderReducer from 'containers/LanguageProvider/reducer';
10 |
11 | /*
12 | * routeReducer
13 | *
14 | * The reducer merges route location changes into our immutable state.
15 | * The change is necessitated by moving to react-router-redux@4
16 | *
17 | */
18 |
19 | // Initial routing state
20 | const routeInitialState = fromJS({
21 | location: null,
22 | });
23 |
24 | /**
25 | * Merge route into the global application state
26 | */
27 | export function routeReducer(state = routeInitialState, action) {
28 | switch (action.type) {
29 | /* istanbul ignore next */
30 | case LOCATION_CHANGE:
31 | return state.merge({
32 | location: action.payload,
33 | });
34 | default:
35 | return state;
36 | }
37 | }
38 |
39 | /**
40 | * Creates the main reducer with the dynamically injected ones
41 | */
42 | export default function createReducer(injectedReducers) {
43 | return combineReducers({
44 | route: routeReducer,
45 | language: languageProviderReducer,
46 | ...injectedReducers,
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/app/containers/HomePage/tests/__snapshots__/saga.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`getRepos Saga should call the repoLoadingError action if the response errors 1`] = `
4 | Object {
5 | "@@redux-saga/IO": true,
6 | "SELECT": Object {
7 | "args": Array [],
8 | "selector": [Function],
9 | },
10 | }
11 | `;
12 |
13 | exports[`getRepos Saga should call the repoLoadingError action if the response errors 2`] = `
14 | Object {
15 | "@@redux-saga/IO": true,
16 | "CALL": Object {
17 | "args": Array [
18 | "https://api.github.com/users/mxstbr/repos?type=all&sort=updated",
19 | ],
20 | "context": null,
21 | "fn": [Function],
22 | },
23 | }
24 | `;
25 |
26 | exports[`getRepos Saga should dispatch the reposLoaded action if it requests the data successfully 1`] = `
27 | Object {
28 | "@@redux-saga/IO": true,
29 | "SELECT": Object {
30 | "args": Array [],
31 | "selector": [Function],
32 | },
33 | }
34 | `;
35 |
36 | exports[`getRepos Saga should dispatch the reposLoaded action if it requests the data successfully 2`] = `
37 | Object {
38 | "@@redux-saga/IO": true,
39 | "CALL": Object {
40 | "args": Array [
41 | "https://api.github.com/users/mxstbr/repos?type=all&sort=updated",
42 | ],
43 | "context": null,
44 | "fn": [Function],
45 | },
46 | }
47 | `;
48 |
--------------------------------------------------------------------------------
/app/utils/request.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 |
3 | /**
4 | * Parses the JSON returned by a network request
5 | *
6 | * @param {object} response A response from a network request
7 | *
8 | * @return {object} The parsed JSON from the request
9 | */
10 | function parseJSON(response) {
11 | if (response.status === 204 || response.status === 205) {
12 | return null;
13 | }
14 | return response.json();
15 | }
16 |
17 | /**
18 | * Checks if a network request came back fine, and throws an error if not
19 | *
20 | * @param {object} response A response from a network request
21 | *
22 | * @return {object|undefined} Returns either the response, or throws an error
23 | */
24 | function checkStatus(response) {
25 | if (response.status >= 200 && response.status < 300) {
26 | return response;
27 | }
28 |
29 | const error = new Error(response.statusText);
30 | error.response = response;
31 | throw error;
32 | }
33 |
34 | /**
35 | * Requests a URL, returning a promise
36 | *
37 | * @param {string} url The URL we want to request
38 | * @param {object} [options] The options we want to pass to "fetch"
39 | *
40 | * @return {object} The response data
41 | */
42 | export default function request(url, options) {
43 | return fetch(url, options)
44 | .then(checkStatus)
45 | .then(parseJSON);
46 | }
47 |
--------------------------------------------------------------------------------
/internals/generators/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * generator/index.js
3 | *
4 | * Exports the generators so plop knows them
5 | */
6 |
7 | const fs = require('fs');
8 | const path = require('path');
9 | const { exec } = require('child_process');
10 | const componentGenerator = require('./component/index.js');
11 | const containerGenerator = require('./container/index.js');
12 | const languageGenerator = require('./language/index.js');
13 |
14 | module.exports = plop => {
15 | plop.setGenerator('component', componentGenerator);
16 | plop.setGenerator('container', containerGenerator);
17 | plop.setGenerator('language', languageGenerator);
18 | plop.addHelper('directory', comp => {
19 | try {
20 | fs.accessSync(
21 | path.join(__dirname, `../../app/containers/${comp}`),
22 | fs.F_OK,
23 | );
24 | return `containers/${comp}`;
25 | } catch (e) {
26 | return `components/${comp}`;
27 | }
28 | });
29 | plop.addHelper('curly', (object, open) => (open ? '{' : '}'));
30 | plop.setActionType('prettify', (answers, config) => {
31 | const folderPath = `${path.join(
32 | __dirname,
33 | '/../../app/',
34 | config.path,
35 | plop.getHelper('properCase')(answers.name),
36 | '**.js',
37 | )}`;
38 | exec(`npm run prettify -- "${folderPath}"`);
39 | return folderPath;
40 | });
41 | };
42 |
--------------------------------------------------------------------------------
/app/containers/App/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | * AppReducer
3 | *
4 | * The reducer takes care of our data. Using actions, we can change our
5 | * application state.
6 | * To add a new action, add it to the switch statement in the reducer function
7 | *
8 | * Example:
9 | * case YOUR_ACTION_CONSTANT:
10 | * return state.set('yourStateVariable', true);
11 | */
12 |
13 | import { fromJS } from 'immutable';
14 |
15 | import { LOAD_REPOS_SUCCESS, LOAD_REPOS, LOAD_REPOS_ERROR } from './constants';
16 |
17 | // The initial state of the App
18 | const initialState = fromJS({
19 | loading: false,
20 | error: false,
21 | currentUser: false,
22 | userData: {
23 | repositories: false,
24 | },
25 | });
26 |
27 | function appReducer(state = initialState, action) {
28 | switch (action.type) {
29 | case LOAD_REPOS:
30 | return state
31 | .set('loading', true)
32 | .set('error', false)
33 | .setIn(['userData', 'repositories'], false);
34 | case LOAD_REPOS_SUCCESS:
35 | return state
36 | .setIn(['userData', 'repositories'], action.repos)
37 | .set('loading', false)
38 | .set('currentUser', action.username);
39 | case LOAD_REPOS_ERROR:
40 | return state.set('error', action.error).set('loading', false);
41 | default:
42 | return state;
43 | }
44 | }
45 |
46 | export default appReducer;
47 |
--------------------------------------------------------------------------------
/app/reducers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Combine all reducers in this file and export the combined reducers.
3 | */
4 |
5 | import { fromJS } from 'immutable';
6 | import { combineReducers } from 'redux-immutable';
7 | import { LOCATION_CHANGE } from 'react-router-redux';
8 |
9 | import globalReducer from 'containers/App/reducer';
10 | import languageProviderReducer from 'containers/LanguageProvider/reducer';
11 |
12 | /*
13 | * routeReducer
14 | *
15 | * The reducer merges route location changes into our immutable state.
16 | * The change is necessitated by moving to react-router-redux@5
17 | *
18 | */
19 |
20 | // Initial routing state
21 | const routeInitialState = fromJS({
22 | location: null,
23 | });
24 |
25 | /**
26 | * Merge route into the global application state
27 | */
28 | export function routeReducer(state = routeInitialState, action) {
29 | switch (action.type) {
30 | /* istanbul ignore next */
31 | case LOCATION_CHANGE:
32 | return state.merge({
33 | location: action.payload,
34 | });
35 | default:
36 | return state;
37 | }
38 | }
39 |
40 | /**
41 | * Creates the main reducer with the dynamically injected ones
42 | */
43 | export default function createReducer(injectedReducers) {
44 | return combineReducers({
45 | route: routeReducer,
46 | global: globalReducer,
47 | language: languageProviderReducer,
48 | ...injectedReducers,
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/app/components/Toggle/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { IntlProvider, defineMessages } from 'react-intl';
4 |
5 | import Toggle from '../index';
6 |
7 | describe(' ', () => {
8 | it('should contain default text', () => {
9 | const defaultEnMessage = 'someContent';
10 | const defaultDeMessage = 'someOtherContent';
11 | const messages = defineMessages({
12 | en: {
13 | id: 'boilerplate.containers.LocaleToggle.en',
14 | defaultMessage: defaultEnMessage,
15 | },
16 | de: {
17 | id: 'boilerplate.containers.LocaleToggle.en',
18 | defaultMessage: defaultDeMessage,
19 | },
20 | });
21 | const renderedComponent = shallow(
22 |
23 |
24 | ,
25 | );
26 | expect(
27 | renderedComponent.contains(
28 | ,
29 | ),
30 | ).toBe(true);
31 | expect(renderedComponent.find('option').length).toBe(0);
32 | });
33 | it('should not have ToggleOptions if props.values is not defined', () => {
34 | const renderedComponent = shallow( );
35 | expect(renderedComponent.contains(-- )).toBe(true);
36 | expect(renderedComponent.find('option').length).toBe(1);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/app/containers/HomePage/saga.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets the repositories of the user from Github
3 | */
4 |
5 | import { call, put, select, takeLatest } from 'redux-saga/effects';
6 | import { LOAD_REPOS } from 'containers/App/constants';
7 | import { reposLoaded, repoLoadingError } from 'containers/App/actions';
8 |
9 | import request from 'utils/request';
10 | import { makeSelectUsername } from 'containers/HomePage/selectors';
11 |
12 | /**
13 | * Github repos request/response handler
14 | */
15 | export function* getRepos() {
16 | // Select username from store
17 | const username = yield select(makeSelectUsername());
18 | const requestURL = `https://api.github.com/users/${username}/repos?type=all&sort=updated`;
19 |
20 | try {
21 | // Call our request helper (see 'utils/request')
22 | const repos = yield call(request, requestURL);
23 | yield put(reposLoaded(repos, username));
24 | } catch (err) {
25 | yield put(repoLoadingError(err));
26 | }
27 | }
28 |
29 | /**
30 | * Root saga manages watcher lifecycle
31 | */
32 | export default function* githubData() {
33 | // Watches for LOAD_REPOS actions and calls getRepos when one comes in.
34 | // By using `takeLatest` only the result of the latest API call is applied.
35 | // It returns task descriptor (just like fork) so we can continue execution
36 | // It will be cancelled automatically on component unmount
37 | yield takeLatest(LOAD_REPOS, getRepos);
38 | }
39 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | # http://www.appveyor.com/docs/appveyor-yml
2 |
3 | # Set build version format here instead of in the admin panel
4 | version: "{build}"
5 |
6 | # Do not build on gh tags
7 | skip_tags: true
8 |
9 | # Test against these versions of Node.js
10 | environment:
11 |
12 | matrix:
13 | # Node versions to run
14 | - nodejs_version: 9
15 | - nodejs_version: 8
16 |
17 | # Fix line endings in Windows. (runs before repo cloning)
18 | init:
19 | - git config --global core.autocrlf input
20 |
21 | # Install scripts--runs after repo cloning
22 | install:
23 | # Install chrome
24 | - choco install -y googlechrome --ignore-checksums
25 | # Install the latest stable version of Node
26 | - ps: Install-Product node $env:nodejs_version
27 | - set PATH=%APPDATA%\yarn;%PATH%
28 | - yarn
29 |
30 | # Disable automatic builds
31 | build: off
32 |
33 | # Post-install test scripts
34 | test_script:
35 | # Output debugging info
36 | - node --version
37 | - node ./internals/scripts/generate-templates-for-linting
38 | # run tests and run build
39 | - yarn run test
40 | - yarn run build
41 |
42 | # Cache node_modules for faster builds
43 | cache:
44 | - "%LOCALAPPDATA%\\Yarn"
45 | - node_modules -> package.json
46 |
47 | # remove, as appveyor doesn't support secure variables on pr builds
48 | # so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file
49 | #on_success:
50 | #- yarn run coveralls
51 |
--------------------------------------------------------------------------------
/internals/templates/containers/LanguageProvider/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * LanguageProvider
4 | *
5 | * this component connects the redux state language locale to the
6 | * IntlProvider component and i18n messages (loaded from `app/translations`)
7 | */
8 |
9 | import React from 'react';
10 | import PropTypes from 'prop-types';
11 | import { connect } from 'react-redux';
12 | import { createSelector } from 'reselect';
13 | import { IntlProvider } from 'react-intl';
14 |
15 | import { makeSelectLocale } from './selectors';
16 |
17 | export class LanguageProvider extends React.PureComponent {
18 | // eslint-disable-line react/prefer-stateless-function
19 | render() {
20 | return (
21 |
26 | {React.Children.only(this.props.children)}
27 |
28 | );
29 | }
30 | }
31 |
32 | LanguageProvider.propTypes = {
33 | locale: PropTypes.string,
34 | messages: PropTypes.object,
35 | children: PropTypes.element.isRequired,
36 | };
37 |
38 | const mapStateToProps = createSelector(makeSelectLocale(), locale => ({
39 | locale,
40 | }));
41 |
42 | function mapDispatchToProps(dispatch) {
43 | return {
44 | dispatch,
45 | };
46 | }
47 |
48 | export default connect(
49 | mapStateToProps,
50 | mapDispatchToProps,
51 | )(LanguageProvider);
52 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.dll.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WEBPACK DLL GENERATOR
3 | *
4 | * This profile is used to cache webpack's module
5 | * contexts for external library and framework type
6 | * dependencies which will usually not change often enough
7 | * to warrant building them from scratch every time we use
8 | * the webpack process.
9 | */
10 |
11 | const { join } = require('path');
12 | const defaults = require('lodash/defaultsDeep');
13 | const webpack = require('webpack');
14 | const pkg = require(join(process.cwd(), 'package.json'));
15 | const { dllPlugin } = require('../config');
16 |
17 | if (!pkg.dllPlugin) {
18 | process.exit(0);
19 | }
20 |
21 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults);
22 | const outputPath = join(process.cwd(), dllConfig.path);
23 |
24 | module.exports = require('./webpack.base.babel')({
25 | mode: 'development',
26 | context: process.cwd(),
27 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg),
28 | optimization: {
29 | minimize: false,
30 | },
31 | devtool: 'eval',
32 | output: {
33 | filename: '[name].dll.js',
34 | path: outputPath,
35 | library: '[name]',
36 | libraryTarget: 'var',
37 | },
38 | plugins: [
39 | new webpack.DllPlugin({
40 | name: '[name]',
41 | path: join(outputPath, '[name].json'),
42 | }),
43 | ],
44 | performance: {
45 | hints: false,
46 | },
47 | target: 'electron-renderer',
48 | });
49 |
--------------------------------------------------------------------------------
/app/containers/LocaleToggle/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * LanguageToggle
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import PropTypes from 'prop-types';
9 | import { connect } from 'react-redux';
10 | import { createSelector } from 'reselect';
11 |
12 | import Toggle from 'components/Toggle';
13 | import Wrapper from './Wrapper';
14 | import messages from './messages';
15 | import { appLocales } from '../../i18n';
16 | import { changeLocale } from '../LanguageProvider/actions';
17 | import { makeSelectLocale } from '../LanguageProvider/selectors';
18 |
19 | export class LocaleToggle extends React.PureComponent {
20 | // eslint-disable-line react/prefer-stateless-function
21 | render() {
22 | return (
23 |
24 |
30 |
31 | );
32 | }
33 | }
34 |
35 | LocaleToggle.propTypes = {
36 | onLocaleToggle: PropTypes.func,
37 | locale: PropTypes.string,
38 | };
39 |
40 | const mapStateToProps = createSelector(makeSelectLocale(), locale => ({
41 | locale,
42 | }));
43 |
44 | export function mapDispatchToProps(dispatch) {
45 | return {
46 | onLocaleToggle: evt => dispatch(changeLocale(evt.target.value)),
47 | dispatch,
48 | };
49 | }
50 |
51 | export default connect(
52 | mapStateToProps,
53 | mapDispatchToProps,
54 | )(LocaleToggle);
55 |
--------------------------------------------------------------------------------
/app/components/Img/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import Img from '../index';
5 |
6 | const src = 'test.png';
7 | const alt = 'test';
8 | const renderComponent = (props = {}) =>
9 | shallow( );
10 |
11 | describe(' ', () => {
12 | it('should render an tag', () => {
13 | const renderedComponent = renderComponent();
14 | expect(renderedComponent.is('img')).toBe(true);
15 | });
16 |
17 | it('should have an src attribute', () => {
18 | const renderedComponent = renderComponent();
19 | expect(renderedComponent.prop('src')).toEqual(src);
20 | });
21 |
22 | it('should have an alt attribute', () => {
23 | const renderedComponent = renderComponent();
24 | expect(renderedComponent.prop('alt')).toEqual(alt);
25 | });
26 |
27 | it('should not have a className attribute', () => {
28 | const renderedComponent = renderComponent();
29 | expect(renderedComponent.prop('className')).toBeUndefined();
30 | });
31 |
32 | it('should adopt a className attribute', () => {
33 | const className = 'test';
34 | const renderedComponent = renderComponent({ className });
35 | expect(renderedComponent.hasClass(className)).toBe(true);
36 | });
37 |
38 | it('should not adopt a srcset attribute', () => {
39 | const srcset = 'test-HD.png 2x';
40 | const renderedComponent = renderComponent({ srcset });
41 | expect(renderedComponent.prop('srcset')).toBeUndefined();
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/app/containers/App/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * App
4 | *
5 | * This component is the skeleton around the actual pages, and should only
6 | * contain code that should be seen on all pages. (e.g. navigation bar)
7 | */
8 |
9 | import React from 'react';
10 | import { Helmet } from 'react-helmet';
11 | import styled from 'styled-components';
12 | import { Switch, Route } from 'react-router-dom';
13 |
14 | import HomePage from 'containers/HomePage/Loadable';
15 | import FeaturePage from 'containers/FeaturePage/Loadable';
16 | import SettingsPage from 'containers/SettingsPage/Loadable';
17 | import NotFoundPage from 'containers/NotFoundPage/Loadable';
18 | import Header from 'components/Header';
19 | import Footer from 'components/Footer';
20 |
21 | const AppWrapper = styled.div`
22 | max-width: calc(768px + 16px * 2);
23 | margin: 0 auto;
24 | display: flex;
25 | min-height: 100%;
26 | padding: 0 16px;
27 | flex-direction: column;
28 | `;
29 |
30 | export default function App() {
31 | return (
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/internals/templates/i18n.js:
--------------------------------------------------------------------------------
1 | /**
2 | * i18n.js
3 | *
4 | * This will setup the i18n language files and locale data for your app.
5 | *
6 | * IMPORTANT: This file is used by the internal build
7 | * script `extract-intl`, and must use CommonJS module syntax
8 | * You CANNOT use import/export in this file.
9 | */
10 | const addLocaleData = require('react-intl').addLocaleData; //eslint-disable-line
11 | const enLocaleData = require('react-intl/locale-data/en');
12 |
13 | const enTranslationMessages = require('./translations/en.json');
14 |
15 | addLocaleData(enLocaleData);
16 |
17 | const DEFAULT_LOCALE = 'en';
18 |
19 | // prettier-ignore
20 | const appLocales = [
21 | 'en',
22 | ];
23 |
24 | const formatTranslationMessages = (locale, messages) => {
25 | const defaultFormattedMessages =
26 | locale !== DEFAULT_LOCALE
27 | ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages)
28 | : {};
29 | const flattenFormattedMessages = (formattedMessages, key) => {
30 | const formattedMessage =
31 | !messages[key] && locale !== DEFAULT_LOCALE
32 | ? defaultFormattedMessages[key]
33 | : messages[key];
34 | return Object.assign(formattedMessages, { [key]: formattedMessage });
35 | };
36 | return Object.keys(messages).reduce(flattenFormattedMessages, {});
37 | };
38 |
39 | const translationMessages = {
40 | en: formatTranslationMessages('en', enTranslationMessages),
41 | };
42 |
43 | exports.appLocales = appLocales;
44 | exports.formatTranslationMessages = formatTranslationMessages;
45 | exports.translationMessages = translationMessages;
46 | exports.DEFAULT_LOCALE = DEFAULT_LOCALE;
47 |
--------------------------------------------------------------------------------
/internals/scripts/dependencies.js:
--------------------------------------------------------------------------------
1 | // No need to build the DLL in production
2 | if (process.env.NODE_ENV === 'production') {
3 | process.exit(0);
4 | }
5 |
6 | require('shelljs/global');
7 |
8 | const path = require('path');
9 | const fs = require('fs');
10 | const exists = fs.existsSync;
11 | const writeFile = fs.writeFileSync;
12 |
13 | const defaults = require('lodash/defaultsDeep');
14 | const pkg = require(path.join(process.cwd(), 'package.json'));
15 | const config = require('../config');
16 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults);
17 | const outputPath = path.join(process.cwd(), dllConfig.path);
18 | const dllManifestPath = path.join(outputPath, 'package.json');
19 |
20 | /**
21 | * I use node_modules/react-boilerplate-dlls by default just because
22 | * it isn't going to be version controlled and babel wont try to parse it.
23 | */
24 | mkdir('-p', outputPath);
25 |
26 | echo('Building the Webpack DLL...');
27 |
28 | /**
29 | * Create a manifest so npm install doesn't warn us
30 | */
31 | if (!exists(dllManifestPath)) {
32 | writeFile(
33 | dllManifestPath,
34 | JSON.stringify(
35 | defaults({
36 | name: 'react-boilerplate-dlls',
37 | private: true,
38 | author: pkg.author,
39 | repository: pkg.repository,
40 | version: pkg.version,
41 | }),
42 | null,
43 | 2,
44 | ),
45 | 'utf8',
46 | );
47 | }
48 |
49 | // the BUILDING_DLL env var is set to avoid confusing the development environment
50 | exec(
51 | 'cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js --hide-modules',
52 | );
53 |
--------------------------------------------------------------------------------
/app/components/A/tests/index.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Testing our link component
3 | */
4 |
5 | import React from 'react';
6 | import { shallow } from 'enzyme';
7 |
8 | import A from '../index';
9 |
10 | const href = 'http://mxstbr.com/';
11 | const children = Test ;
12 | const renderComponent = (props = {}) =>
13 | shallow(
14 |
15 | {children}
16 | ,
17 | );
18 |
19 | describe(' ', () => {
20 | it('should render an tag', () => {
21 | const renderedComponent = renderComponent();
22 | expect(renderedComponent.type()).toEqual('a');
23 | });
24 |
25 | it('should have an href attribute', () => {
26 | const renderedComponent = renderComponent();
27 | expect(renderedComponent.prop('href')).toEqual(href);
28 | });
29 |
30 | it('should have children', () => {
31 | const renderedComponent = renderComponent();
32 | expect(renderedComponent.contains(children)).toEqual(true);
33 | });
34 |
35 | it('should have a className attribute', () => {
36 | const className = 'test';
37 | const renderedComponent = renderComponent({ className });
38 | expect(renderedComponent.find('a').hasClass(className)).toEqual(true);
39 | });
40 |
41 | it('should adopt a target attribute', () => {
42 | const target = '_blank';
43 | const renderedComponent = renderComponent({ target });
44 | expect(renderedComponent.prop('target')).toEqual(target);
45 | });
46 |
47 | it('should adopt a type attribute', () => {
48 | const type = 'text/html';
49 | const renderedComponent = renderComponent({ type });
50 | expect(renderedComponent.prop('type')).toEqual(type);
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/app/utils/injectSaga.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import hoistNonReactStatics from 'hoist-non-react-statics';
4 |
5 | import getInjectors from './sagaInjectors';
6 |
7 | /**
8 | * Dynamically injects a saga, passes component's props as saga arguments
9 | *
10 | * @param {string} key A key of the saga
11 | * @param {function} saga A root saga that will be injected
12 | * @param {string} [mode] By default (constants.RESTART_ON_REMOUNT) the saga will be started on component mount and
13 | * cancelled with `task.cancel()` on component un-mount for improved performance. Another two options:
14 | * - constants.DAEMON—starts the saga on component mount and never cancels it or starts again,
15 | * - constants.ONCE_TILL_UNMOUNT—behaves like 'RESTART_ON_REMOUNT' but never runs it again.
16 | *
17 | */
18 | export default ({ key, saga, mode }) => WrappedComponent => {
19 | class InjectSaga extends React.Component {
20 | static WrappedComponent = WrappedComponent;
21 | static contextTypes = {
22 | store: PropTypes.object.isRequired,
23 | };
24 | static displayName = `withSaga(${WrappedComponent.displayName ||
25 | WrappedComponent.name ||
26 | 'Component'})`;
27 |
28 | componentWillMount() {
29 | const { injectSaga } = this.injectors;
30 |
31 | injectSaga(key, { saga, mode }, this.props);
32 | }
33 |
34 | componentWillUnmount() {
35 | const { ejectSaga } = this.injectors;
36 |
37 | ejectSaga(key);
38 | }
39 |
40 | injectors = getInjectors(this.context.store);
41 |
42 | render() {
43 | return ;
44 | }
45 | }
46 |
47 | return hoistNonReactStatics(InjectSaga, WrappedComponent);
48 | };
49 |
--------------------------------------------------------------------------------
/app/containers/LanguageProvider/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow, mount } from 'enzyme';
3 | import { FormattedMessage, defineMessages } from 'react-intl';
4 | import { Provider } from 'react-redux';
5 | import { browserHistory } from 'react-router-dom';
6 |
7 | import ConnectedLanguageProvider, { LanguageProvider } from '../index';
8 | import configureStore from '../../../configureStore';
9 |
10 | import { translationMessages } from '../../../i18n';
11 |
12 | const messages = defineMessages({
13 | someMessage: {
14 | id: 'some.id',
15 | defaultMessage: 'This is some default message',
16 | en: 'This is some en message',
17 | },
18 | });
19 |
20 | describe(' ', () => {
21 | it('should render its children', () => {
22 | const children = Test ;
23 | const renderedComponent = shallow(
24 |
25 | {children}
26 | ,
27 | );
28 | expect(renderedComponent.contains(children)).toBe(true);
29 | });
30 | });
31 |
32 | describe(' ', () => {
33 | let store;
34 |
35 | beforeAll(() => {
36 | store = configureStore({}, browserHistory);
37 | });
38 |
39 | it('should render the default language messages', () => {
40 | const renderedComponent = mount(
41 |
42 |
43 |
44 |
45 | ,
46 | );
47 | expect(
48 | renderedComponent.contains(
49 | ,
50 | ),
51 | ).toBe(true);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | React.js Boilerplate
22 |
23 |
24 |
25 | If you're seeing this message, that means JavaScript has been disabled on your browser , please enable JS to make this app work.
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/internals/templates/utils/injectSaga.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import hoistNonReactStatics from 'hoist-non-react-statics';
4 |
5 | import getInjectors from './sagaInjectors';
6 |
7 | /**
8 | * Dynamically injects a saga, passes component's props as saga arguments
9 | *
10 | * @param {string} key A key of the saga
11 | * @param {function} saga A root saga that will be injected
12 | * @param {string} [mode] By default (constants.RESTART_ON_REMOUNT) the saga will be started on component mount and
13 | * cancelled with `task.cancel()` on component un-mount for improved performance. Another two options:
14 | * - constants.DAEMON—starts the saga on component mount and never cancels it or starts again,
15 | * - constants.ONCE_TILL_UNMOUNT—behaves like 'RESTART_ON_REMOUNT' but never runs it again.
16 | *
17 | */
18 | export default ({ key, saga, mode }) => WrappedComponent => {
19 | class InjectSaga extends React.Component {
20 | static WrappedComponent = WrappedComponent;
21 | static contextTypes = {
22 | store: PropTypes.object.isRequired,
23 | };
24 | static displayName = `withSaga(${WrappedComponent.displayName ||
25 | WrappedComponent.name ||
26 | 'Component'})`;
27 |
28 | componentWillMount() {
29 | const { injectSaga } = this.injectors;
30 |
31 | injectSaga(key, { saga, mode }, this.props);
32 | }
33 |
34 | componentWillUnmount() {
35 | const { ejectSaga } = this.injectors;
36 |
37 | ejectSaga(key);
38 | }
39 |
40 | injectors = getInjectors(this.context.store);
41 |
42 | render() {
43 | return ;
44 | }
45 | }
46 |
47 | return hoistNonReactStatics(InjectSaga, WrappedComponent);
48 | };
49 |
--------------------------------------------------------------------------------
/app/containers/App/actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | * App Actions
3 | *
4 | * Actions change things in your application
5 | * Since this boilerplate uses a uni-directional data flow, specifically redux,
6 | * we have these actions which are the only way your application interacts with
7 | * your application state. This guarantees that your state is up to date and nobody
8 | * messes it up weirdly somewhere.
9 | *
10 | * To add a new Action:
11 | * 1) Import your constant
12 | * 2) Add a function like this:
13 | * export function yourAction(var) {
14 | * return { type: YOUR_ACTION_CONSTANT, var: var }
15 | * }
16 | */
17 |
18 | import { LOAD_REPOS, LOAD_REPOS_SUCCESS, LOAD_REPOS_ERROR } from './constants';
19 |
20 | /**
21 | * Load the repositories, this action starts the request saga
22 | *
23 | * @return {object} An action object with a type of LOAD_REPOS
24 | */
25 | export function loadRepos() {
26 | return {
27 | type: LOAD_REPOS,
28 | };
29 | }
30 |
31 | /**
32 | * Dispatched when the repositories are loaded by the request saga
33 | *
34 | * @param {array} repos The repository data
35 | * @param {string} username The current username
36 | *
37 | * @return {object} An action object with a type of LOAD_REPOS_SUCCESS passing the repos
38 | */
39 | export function reposLoaded(repos, username) {
40 | return {
41 | type: LOAD_REPOS_SUCCESS,
42 | repos,
43 | username,
44 | };
45 | }
46 |
47 | /**
48 | * Dispatched when loading the repositories fails
49 | *
50 | * @param {object} error The error
51 | *
52 | * @return {object} An action object with a type of LOAD_REPOS_ERROR passing the error
53 | */
54 | export function repoLoadingError(error) {
55 | return {
56 | type: LOAD_REPOS_ERROR,
57 | error,
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/app/containers/App/tests/reducer.test.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import appReducer from '../reducer';
4 | import { loadRepos, reposLoaded, repoLoadingError } from '../actions';
5 |
6 | describe('appReducer', () => {
7 | let state;
8 | beforeEach(() => {
9 | state = fromJS({
10 | loading: false,
11 | error: false,
12 | currentUser: false,
13 | userData: fromJS({
14 | repositories: false,
15 | }),
16 | });
17 | });
18 |
19 | it('should return the initial state', () => {
20 | const expectedResult = state;
21 | expect(appReducer(undefined, {})).toEqual(expectedResult);
22 | });
23 |
24 | it('should handle the loadRepos action correctly', () => {
25 | const expectedResult = state
26 | .set('loading', true)
27 | .set('error', false)
28 | .setIn(['userData', 'repositories'], false);
29 |
30 | expect(appReducer(state, loadRepos())).toEqual(expectedResult);
31 | });
32 |
33 | it('should handle the reposLoaded action correctly', () => {
34 | const fixture = [
35 | {
36 | name: 'My Repo',
37 | },
38 | ];
39 | const username = 'test';
40 | const expectedResult = state
41 | .setIn(['userData', 'repositories'], fixture)
42 | .set('loading', false)
43 | .set('currentUser', username);
44 |
45 | expect(appReducer(state, reposLoaded(fixture, username))).toEqual(
46 | expectedResult,
47 | );
48 | });
49 |
50 | it('should handle the repoLoadingError action correctly', () => {
51 | const fixture = {
52 | msg: 'Not found',
53 | };
54 | const expectedResult = state.set('error', fixture).set('loading', false);
55 |
56 | expect(appReducer(state, repoLoadingError(fixture))).toEqual(
57 | expectedResult,
58 | );
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/internals/generators/container/index.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import PropTypes from 'prop-types';
9 | import { connect } from 'react-redux';
10 | {{#if wantHeaders}}
11 | import { Helmet } from 'react-helmet';
12 | {{/if}}
13 | {{#if wantMessages}}
14 | import { FormattedMessage } from 'react-intl';
15 | {{/if}}
16 | {{#if wantActionsAndReducer}}
17 | import { createStructuredSelector } from 'reselect';
18 | import makeSelect{{properCase name}} from './selectors';
19 | {{/if}}
20 | {{#if wantMessages}}
21 | import messages from './messages';
22 | {{/if}}
23 |
24 | /* eslint-disable react/prefer-stateless-function */
25 | export class {{ properCase name }} extends React.{{{ component }}} {
26 | render() {
27 | return (
28 |
29 | {{#if wantHeaders}}
30 |
31 | {{properCase name}}
32 |
36 |
37 | {{/if}}
38 | {{#if wantMessages}}
39 |
40 | {{/if}}
41 |
42 | );
43 | }
44 | }
45 |
46 | {{ properCase name }}.propTypes = {
47 | dispatch: PropTypes.func.isRequired,
48 | };
49 |
50 | {{#if wantActionsAndReducer}}
51 | const mapStateToProps = createStructuredSelector({
52 | {{name}}: makeSelect{{properCase name}}(),
53 | });
54 | {{/if}}
55 |
56 | function mapDispatchToProps(dispatch) {
57 | return {
58 | dispatch,
59 | };
60 | }
61 |
62 | {{#if wantActionsAndReducer}}
63 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }});
64 | {{else}}
65 | export default connect(null, mapDispatchToProps)({{ properCase name }});
66 | {{/if}}
67 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.main.prod.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import webpack from 'webpack';
3 | import UglifyJSPlugin from 'uglifyjs-webpack-plugin';
4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
5 | import baseConfig from './webpack.base.babel';
6 |
7 | export default baseConfig({
8 | devtool: 'source-map',
9 |
10 | mode: 'production',
11 |
12 | target: 'electron-main',
13 |
14 | entry: path.resolve(process.cwd(), 'app/electron/main.dev.js'),
15 |
16 | output: {
17 | path: path.resolve(process.cwd(), 'app/electron/'),
18 | filename: './main.prod.js',
19 | },
20 |
21 | optimization: {
22 | minimizer: [
23 | new UglifyJSPlugin({
24 | parallel: true,
25 | sourceMap: true,
26 | cache: true,
27 | }),
28 | ],
29 | },
30 |
31 | plugins: [
32 | new BundleAnalyzerPlugin({
33 | analyzerMode:
34 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
35 | openAnalyzer: process.env.OPEN_ANALYZER === 'true',
36 | }),
37 |
38 | /**
39 | * Create global constants which can be configured at compile time.
40 | *
41 | * Useful for allowing different behaviour between development builds and
42 | * release builds
43 | *
44 | * NODE_ENV should be production so that modules do not perform certain
45 | * development checks
46 | */
47 | new webpack.EnvironmentPlugin({
48 | NODE_ENV: 'production',
49 | DEBUG_PROD: false,
50 | START_MINIMIZED: false,
51 | }),
52 | ],
53 |
54 | /**
55 | * Disables webpack processing of __dirname and __filename.
56 | * If you run the bundle in node.js it falls back to these values of node.js.
57 | * https://github.com/webpack/webpack/issues/2010
58 | */
59 | node: {
60 | __dirname: false,
61 | __filename: false,
62 | },
63 | });
64 |
--------------------------------------------------------------------------------
/app/i18n.js:
--------------------------------------------------------------------------------
1 | /**
2 | * i18n.js
3 | *
4 | * This will setup the i18n language files and locale data for your app.
5 | *
6 | * IMPORTANT: This file is used by the internal build
7 | * script `extract-intl`, and must use CommonJS module syntax
8 | * You CANNOT use import/export in this file.
9 | */
10 | const addLocaleData = require('react-intl').addLocaleData; //eslint-disable-line
11 | const enLocaleData = require('react-intl/locale-data/en');
12 | const deLocaleData = require('react-intl/locale-data/de');
13 |
14 | const enTranslationMessages = require('./translations/en.json');
15 | const deTranslationMessages = require('./translations/de.json');
16 |
17 | addLocaleData(enLocaleData);
18 | addLocaleData(deLocaleData);
19 |
20 | const DEFAULT_LOCALE = 'en';
21 |
22 | // prettier-ignore
23 | const appLocales = [
24 | 'en',
25 | 'de',
26 | ];
27 |
28 | const formatTranslationMessages = (locale, messages) => {
29 | const defaultFormattedMessages =
30 | locale !== DEFAULT_LOCALE
31 | ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages)
32 | : {};
33 | const flattenFormattedMessages = (formattedMessages, key) => {
34 | const formattedMessage =
35 | !messages[key] && locale !== DEFAULT_LOCALE
36 | ? defaultFormattedMessages[key]
37 | : messages[key];
38 | return Object.assign(formattedMessages, { [key]: formattedMessage });
39 | };
40 | return Object.keys(messages).reduce(flattenFormattedMessages, {});
41 | };
42 |
43 | const translationMessages = {
44 | en: formatTranslationMessages('en', enTranslationMessages),
45 | de: formatTranslationMessages('de', deTranslationMessages),
46 | };
47 |
48 | exports.appLocales = appLocales;
49 | exports.formatTranslationMessages = formatTranslationMessages;
50 | exports.translationMessages = translationMessages;
51 | exports.DEFAULT_LOCALE = DEFAULT_LOCALE;
52 |
--------------------------------------------------------------------------------
/app/utils/tests/injectReducer.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test injectors
3 | */
4 |
5 | import { memoryHistory } from 'react-router-dom';
6 | import { shallow } from 'enzyme';
7 | import React from 'react';
8 | import identity from 'lodash/identity';
9 |
10 | import configureStore from '../../configureStore';
11 | import injectReducer from '../injectReducer';
12 | import * as reducerInjectors from '../reducerInjectors';
13 |
14 | // Fixtures
15 | const Component = () => null;
16 |
17 | const reducer = identity;
18 |
19 | describe('injectReducer decorator', () => {
20 | let store;
21 | let injectors;
22 | let ComponentWithReducer;
23 |
24 | beforeAll(() => {
25 | reducerInjectors.default = jest.fn().mockImplementation(() => injectors);
26 | });
27 |
28 | beforeEach(() => {
29 | store = configureStore({}, memoryHistory);
30 | injectors = {
31 | injectReducer: jest.fn(),
32 | };
33 | ComponentWithReducer = injectReducer({ key: 'test', reducer })(Component);
34 | reducerInjectors.default.mockClear();
35 | });
36 |
37 | it('should inject a given reducer', () => {
38 | shallow( , { context: { store } });
39 |
40 | expect(injectors.injectReducer).toHaveBeenCalledTimes(1);
41 | expect(injectors.injectReducer).toHaveBeenCalledWith('test', reducer);
42 | });
43 |
44 | it('should set a correct display name', () => {
45 | expect(ComponentWithReducer.displayName).toBe('withReducer(Component)');
46 | expect(
47 | injectReducer({ key: 'test', reducer })(() => null).displayName,
48 | ).toBe('withReducer(Component)');
49 | });
50 |
51 | it('should propagate props', () => {
52 | const props = { testProp: 'test' };
53 | const renderedComponent = shallow( , {
54 | context: { store },
55 | });
56 |
57 | expect(renderedComponent.prop('testProp')).toBe('test');
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/app/containers/RepoListItem/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * RepoListItem
3 | *
4 | * Lists the name and the issue count of a repository
5 | */
6 |
7 | import React from 'react';
8 | import PropTypes from 'prop-types';
9 | import { connect } from 'react-redux';
10 | import { createStructuredSelector } from 'reselect';
11 | import { FormattedNumber } from 'react-intl';
12 |
13 | import { makeSelectCurrentUser } from 'containers/App/selectors';
14 | import ListItem from 'components/ListItem';
15 | import IssueIcon from './IssueIcon';
16 | import IssueLink from './IssueLink';
17 | import RepoLink from './RepoLink';
18 | import Wrapper from './Wrapper';
19 |
20 | export class RepoListItem extends React.PureComponent {
21 | render() {
22 | const { item } = this.props;
23 | let nameprefix = '';
24 |
25 | // If the repository is owned by a different person than we got the data for
26 | // it's a fork and we should show the name of the owner
27 | if (item.owner.login !== this.props.currentUser) {
28 | nameprefix = `${item.owner.login}/`;
29 | }
30 |
31 | // Put together the content of the repository
32 | const content = (
33 |
34 |
35 | {nameprefix + item.name}
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 |
44 | // Render the content into a list item
45 | return ;
46 | }
47 | }
48 |
49 | RepoListItem.propTypes = {
50 | item: PropTypes.object,
51 | currentUser: PropTypes.string,
52 | };
53 |
54 | export default connect(
55 | createStructuredSelector({
56 | currentUser: makeSelectCurrentUser(),
57 | }),
58 | )(RepoListItem);
59 |
--------------------------------------------------------------------------------
/internals/templates/utils/tests/injectReducer.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test injectors
3 | */
4 |
5 | import { memoryHistory } from 'react-router-dom';
6 | import { shallow } from 'enzyme';
7 | import React from 'react';
8 | import identity from 'lodash/identity';
9 |
10 | import configureStore from '../../configureStore';
11 | import injectReducer from '../injectReducer';
12 | import * as reducerInjectors from '../reducerInjectors';
13 |
14 | // Fixtures
15 | const Component = () => null;
16 |
17 | const reducer = identity;
18 |
19 | describe('injectReducer decorator', () => {
20 | let store;
21 | let injectors;
22 | let ComponentWithReducer;
23 |
24 | beforeAll(() => {
25 | reducerInjectors.default = jest.fn().mockImplementation(() => injectors);
26 | });
27 |
28 | beforeEach(() => {
29 | store = configureStore({}, memoryHistory);
30 | injectors = {
31 | injectReducer: jest.fn(),
32 | };
33 | ComponentWithReducer = injectReducer({ key: 'test', reducer })(Component);
34 | reducerInjectors.default.mockClear();
35 | });
36 |
37 | it('should inject a given reducer', () => {
38 | shallow( , { context: { store } });
39 |
40 | expect(injectors.injectReducer).toHaveBeenCalledTimes(1);
41 | expect(injectors.injectReducer).toHaveBeenCalledWith('test', reducer);
42 | });
43 |
44 | it('should set a correct display name', () => {
45 | expect(ComponentWithReducer.displayName).toBe('withReducer(Component)');
46 | expect(
47 | injectReducer({ key: 'test', reducer })(() => null).displayName,
48 | ).toBe('withReducer(Component)');
49 | });
50 |
51 | it('should propagate props', () => {
52 | const props = { testProp: 'test' };
53 | const renderedComponent = shallow( , {
54 | context: { store },
55 | });
56 |
57 | expect(renderedComponent.prop('testProp')).toBe('test');
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/app/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #######################################################################
5 | # GENERAL #
6 | #######################################################################
7 |
8 | # Make apache follow sym links to files
9 | Options +FollowSymLinks
10 | # If somebody opens a folder, hide all files from the resulting folder list
11 | IndexIgnore */*
12 |
13 |
14 | #######################################################################
15 | # REWRITING #
16 | #######################################################################
17 |
18 | # Enable rewriting
19 | RewriteEngine On
20 |
21 | # If its not HTTPS
22 | RewriteCond %{HTTPS} off
23 |
24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL
25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https
26 |
27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect
28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L]
29 |
30 | # If we get to here, it means we are on https://
31 |
32 | # If the file with the specified name in the browser doesn't exist
33 | RewriteCond %{REQUEST_FILENAME} !-f
34 |
35 | # and the directory with the specified name in the browser doesn't exist
36 | RewriteCond %{REQUEST_FILENAME} !-d
37 |
38 | # and we are not opening the root already (otherwise we get a redirect loop)
39 | RewriteCond %{REQUEST_FILENAME} !\/$
40 |
41 | # Rewrite all requests to the root
42 | RewriteRule ^(.*) /
43 |
44 |
45 |
46 |
47 | # Do not cache sw.js, required for offline-first updates.
48 |
49 | Header set Cache-Control "private, no-cache, no-store, proxy-revalidate, no-transform"
50 | Header set Pragma "no-cache"
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/components/ReposList/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { shallow, mount } from 'enzyme';
2 | import React from 'react';
3 | import { IntlProvider } from 'react-intl';
4 |
5 | import RepoListItem from 'containers/RepoListItem';
6 | import List from 'components/List';
7 | import LoadingIndicator from 'components/LoadingIndicator';
8 | import ReposList from '../index';
9 |
10 | describe(' ', () => {
11 | it('should render the loading indicator when its loading', () => {
12 | const renderedComponent = shallow( );
13 | expect(
14 | renderedComponent.contains(
),
15 | ).toEqual(true);
16 | });
17 |
18 | it('should render an error if loading failed', () => {
19 | const renderedComponent = mount(
20 |
21 |
22 | ,
23 | );
24 | expect(renderedComponent.text()).toMatch(/Something went wrong/);
25 | });
26 |
27 | it('should render the repositories if loading was successful', () => {
28 | const repos = [
29 | {
30 | owner: {
31 | login: 'mxstbr',
32 | },
33 | html_url: 'https://github.com/react-boilerplate/react-boilerplate',
34 | name: 'react-boilerplate',
35 | open_issues_count: 20,
36 | full_name: 'react-boilerplate/react-boilerplate',
37 | },
38 | ];
39 | const renderedComponent = shallow(
40 | ,
41 | );
42 |
43 | expect(
44 | renderedComponent.contains(
45 |
,
46 | ),
47 | ).toEqual(true);
48 | });
49 |
50 | it('should not render anything if nothing interesting is provided', () => {
51 | const renderedComponent = shallow(
52 | ,
53 | );
54 |
55 | expect(renderedComponent.html()).toEqual(null);
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/internals/config.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const pullAll = require('lodash/pullAll');
3 | const uniq = require('lodash/uniq');
4 |
5 | const ReactBoilerplate = {
6 | // This refers to the react-boilerplate version this project is based on.
7 | version: '3.6.0',
8 |
9 | /**
10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading
11 | * by caching the module metadata for all of our npm dependencies. We enable it by default
12 | * in development.
13 | *
14 | *
15 | * To disable the DLL Plugin, set this value to false.
16 | */
17 | dllPlugin: {
18 | defaults: {
19 | /**
20 | * we need to exclude dependencies which are not intended for the browser
21 | * by listing them here.
22 | */
23 | exclude: [
24 | 'chalk',
25 | 'compression',
26 | 'cross-env',
27 | 'express',
28 | 'ip',
29 | 'minimist',
30 | 'sanitize.css',
31 | ],
32 |
33 | /**
34 | * Specify any additional dependencies here. We include core-js and lodash
35 | * since a lot of our dependencies depend on them and they get picked up by webpack.
36 | */
37 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'],
38 |
39 | // The path where the DLL manifest and bundle will get built
40 | path: resolve('../node_modules/react-boilerplate-dlls'),
41 | },
42 |
43 | entry(pkg) {
44 | const dependencyNames = Object.keys(pkg.dependencies);
45 | const exclude =
46 | pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude;
47 | const include =
48 | pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include;
49 | const includeDependencies = uniq(dependencyNames.concat(include));
50 |
51 | return {
52 | reactBoilerplateDeps: pullAll(includeDependencies, exclude),
53 | };
54 | },
55 | },
56 | };
57 |
58 | module.exports = ReactBoilerplate;
59 |
--------------------------------------------------------------------------------
/app/containers/HomePage/tests/saga.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Tests for HomePage sagas
3 | */
4 |
5 | import { put, takeLatest } from 'redux-saga/effects';
6 |
7 | import { LOAD_REPOS } from 'containers/App/constants';
8 | import { reposLoaded, repoLoadingError } from 'containers/App/actions';
9 |
10 | import githubData, { getRepos } from '../saga';
11 |
12 | const username = 'mxstbr';
13 |
14 | /* eslint-disable redux-saga/yield-effects */
15 | describe('getRepos Saga', () => {
16 | let getReposGenerator;
17 |
18 | // We have to test twice, once for a successful load and once for an unsuccessful one
19 | // so we do all the stuff that happens beforehand automatically in the beforeEach
20 | beforeEach(() => {
21 | getReposGenerator = getRepos();
22 |
23 | const selectDescriptor = getReposGenerator.next().value;
24 | expect(selectDescriptor).toMatchSnapshot();
25 |
26 | const callDescriptor = getReposGenerator.next(username).value;
27 | expect(callDescriptor).toMatchSnapshot();
28 | });
29 |
30 | it('should dispatch the reposLoaded action if it requests the data successfully', () => {
31 | const response = [
32 | {
33 | name: 'First repo',
34 | },
35 | {
36 | name: 'Second repo',
37 | },
38 | ];
39 | const putDescriptor = getReposGenerator.next(response).value;
40 | expect(putDescriptor).toEqual(put(reposLoaded(response, username)));
41 | });
42 |
43 | it('should call the repoLoadingError action if the response errors', () => {
44 | const response = new Error('Some error');
45 | const putDescriptor = getReposGenerator.throw(response).value;
46 | expect(putDescriptor).toEqual(put(repoLoadingError(response)));
47 | });
48 | });
49 |
50 | describe('githubDataSaga Saga', () => {
51 | const githubDataSaga = githubData();
52 |
53 | it('should start task to watch for LOAD_REPOS action', () => {
54 | const takeLatestDescriptor = githubDataSaga.next().value;
55 | expect(takeLatestDescriptor).toEqual(takeLatest(LOAD_REPOS, getRepos));
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
2 |
3 | # Handle line endings automatically for files detected as text
4 | # and leave all files detected as binary untouched.
5 | * text=auto
6 |
7 | #
8 | # The above will handle all files NOT found below
9 | #
10 |
11 | #
12 | ## These files are text and should be normalized (Convert crlf => lf)
13 | #
14 |
15 | # source code
16 | *.php text
17 | *.css text
18 | *.sass text
19 | *.scss text
20 | *.less text
21 | *.styl text
22 | *.js text eol=lf
23 | *.coffee text
24 | *.json text
25 | *.htm text
26 | *.html text
27 | *.xml text
28 | *.svg text
29 | *.txt text
30 | *.ini text
31 | *.inc text
32 | *.pl text
33 | *.rb text
34 | *.py text
35 | *.scm text
36 | *.sql text
37 | *.sh text
38 | *.bat text
39 |
40 | # templates
41 | *.ejs text
42 | *.hbt text
43 | *.jade text
44 | *.haml text
45 | *.hbs text
46 | *.dot text
47 | *.tmpl text
48 | *.phtml text
49 |
50 | # server config
51 | .htaccess text
52 | .nginx.conf text
53 |
54 | # git config
55 | .gitattributes text
56 | .gitignore text
57 | .gitconfig text
58 |
59 | # code analysis config
60 | .jshintrc text
61 | .jscsrc text
62 | .jshintignore text
63 | .csslintrc text
64 |
65 | # misc config
66 | *.yaml text
67 | *.yml text
68 | .editorconfig text
69 |
70 | # build config
71 | *.npmignore text
72 | *.bowerrc text
73 |
74 | # Heroku
75 | Procfile text
76 | .slugignore text
77 |
78 | # Documentation
79 | *.md text
80 | LICENSE text
81 | AUTHORS text
82 |
83 |
84 | #
85 | ## These files are binary and should be left untouched
86 | #
87 |
88 | # (binary is a macro for -text -diff)
89 | *.png binary
90 | *.jpg binary
91 | *.jpeg binary
92 | *.gif binary
93 | *.ico binary
94 | *.mov binary
95 | *.mp4 binary
96 | *.mp3 binary
97 | *.flv binary
98 | *.fla binary
99 | *.swf binary
100 | *.gz binary
101 | *.zip binary
102 | *.7z binary
103 | *.ttf binary
104 | *.eot binary
105 | *.woff binary
106 | *.pyc binary
107 | *.pdf binary
108 |
--------------------------------------------------------------------------------
/internals/scripts/clean.js:
--------------------------------------------------------------------------------
1 | const shell = require('shelljs');
2 | const addCheckMark = require('./helpers/checkmark.js');
3 |
4 | if (!shell.which('git')) {
5 | shell.echo('Sorry, this script requires git');
6 | shell.exit(1);
7 | }
8 |
9 | if (!shell.test('-e', 'internals/templates')) {
10 | shell.echo('The example is deleted already.');
11 | shell.exit(1);
12 | }
13 |
14 | process.stdout.write('Cleanup started...');
15 |
16 | // Reuse existing LanguageProvider and i18n tests
17 | shell.mv(
18 | 'app/containers/LanguageProvider/tests',
19 | 'internals/templates/containers/LanguageProvider',
20 | );
21 | shell.cp('app/tests/i18n.test.js', 'internals/templates/tests/i18n.test.js');
22 |
23 | // Cleanup components/
24 | shell.rm('-rf', 'app/components/*');
25 |
26 | // Handle containers/
27 | shell.rm('-rf', 'app/containers');
28 | shell.mv('internals/templates/containers', 'app');
29 |
30 | // Handle tests/
31 | shell.mv('internals/templates/tests', 'app');
32 |
33 | // Handle translations/
34 | shell.rm('-rf', 'app/translations');
35 | shell.mv('internals/templates/translations', 'app');
36 |
37 | // Handle utils/
38 | shell.rm('-rf', 'app/utils');
39 | shell.mv('internals/templates/utils', 'app');
40 |
41 | // Replace the files in the root app/ folder
42 | shell.cp('internals/templates/app.js', 'app/app.js');
43 | shell.cp('internals/templates/global-styles.js', 'app/global-styles.js');
44 | shell.cp('internals/templates/i18n.js', 'app/i18n.js');
45 | shell.cp('internals/templates/index.html', 'app/index.html');
46 | shell.cp('internals/templates/reducers.js', 'app/reducers.js');
47 | shell.cp('internals/templates/configureStore.js', 'app/configureStore.js');
48 |
49 | // Remove the templates folder
50 | shell.rm('-rf', 'internals/templates');
51 |
52 | addCheckMark();
53 |
54 | // Commit the changes
55 | if (
56 | shell.exec('git add . --all && git commit -qm "Remove default example"')
57 | .code !== 0
58 | ) {
59 | shell.echo('\nError: Git commit failed');
60 | shell.exit(1);
61 | }
62 |
63 | shell.echo('\nCleanup done. Happy Coding!!!');
64 |
--------------------------------------------------------------------------------
/app/configureStore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create the store with dynamic reducers
3 | */
4 |
5 | import { createStore, applyMiddleware, compose } from 'redux';
6 | import { fromJS } from 'immutable';
7 | import { routerMiddleware } from 'react-router-redux';
8 | import createSagaMiddleware from 'redux-saga';
9 | import createReducer from './reducers';
10 |
11 | const sagaMiddleware = createSagaMiddleware();
12 |
13 | export default function configureStore(initialState = {}, history) {
14 | // Create the store with two middlewares
15 | // 1. sagaMiddleware: Makes redux-sagas work
16 | // 2. routerMiddleware: Syncs the location/URL path to the state
17 | const middlewares = [sagaMiddleware, routerMiddleware(history)];
18 |
19 | const enhancers = [applyMiddleware(...middlewares)];
20 |
21 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose
22 | /* eslint-disable no-underscore-dangle, indent */
23 | const composeEnhancers =
24 | process.env.NODE_ENV !== 'production' &&
25 | typeof window === 'object' &&
26 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
27 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
28 | // TODO: Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading
29 | // Prevent recomputing reducers for `replaceReducer`
30 | shouldHotReload: false,
31 | })
32 | : compose;
33 | /* eslint-enable */
34 |
35 | const store = createStore(
36 | createReducer(),
37 | fromJS(initialState),
38 | composeEnhancers(...enhancers),
39 | );
40 |
41 | // Extensions
42 | store.runSaga = sagaMiddleware.run;
43 | store.injectedReducers = {}; // Reducer registry
44 | store.injectedSagas = {}; // Saga registry
45 |
46 | // Make reducers hot reloadable, see http://mxs.is/googmo
47 | /* istanbul ignore next */
48 | if (module.hot) {
49 | module.hot.accept('./reducers', () => {
50 | store.replaceReducer(createReducer(store.injectedReducers));
51 | });
52 | }
53 |
54 | return store;
55 | }
56 |
--------------------------------------------------------------------------------
/internals/templates/configureStore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create the store with dynamic reducers
3 | */
4 |
5 | import { createStore, applyMiddleware, compose } from 'redux';
6 | import { fromJS } from 'immutable';
7 | import { routerMiddleware } from 'react-router-redux';
8 | import createSagaMiddleware from 'redux-saga';
9 | import createReducer from './reducers';
10 |
11 | const sagaMiddleware = createSagaMiddleware();
12 |
13 | export default function configureStore(initialState = {}, history) {
14 | // Create the store with two middlewares
15 | // 1. sagaMiddleware: Makes redux-sagas work
16 | // 2. routerMiddleware: Syncs the location/URL path to the state
17 | const middlewares = [sagaMiddleware, routerMiddleware(history)];
18 |
19 | const enhancers = [applyMiddleware(...middlewares)];
20 |
21 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose
22 | /* eslint-disable no-underscore-dangle, indent */
23 | const composeEnhancers =
24 | process.env.NODE_ENV !== 'production' &&
25 | typeof window === 'object' &&
26 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
27 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
28 | // TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading
29 | // Prevent recomputing reducers for `replaceReducer`
30 | shouldHotReload: false,
31 | })
32 | : compose;
33 | /* eslint-enable */
34 |
35 | const store = createStore(
36 | createReducer(),
37 | fromJS(initialState),
38 | composeEnhancers(...enhancers),
39 | );
40 |
41 | // Extensions
42 | store.runSaga = sagaMiddleware.run;
43 | store.injectedReducers = {}; // Reducer registry
44 | store.injectedSagas = {}; // Saga registry
45 |
46 | // Make reducers hot reloadable, see http://mxs.is/googmo
47 | /* istanbul ignore next */
48 | if (module.hot) {
49 | module.hot.accept('./reducers', () => {
50 | store.replaceReducer(createReducer(store.injectedReducers));
51 | });
52 | }
53 |
54 | return store;
55 | }
56 |
--------------------------------------------------------------------------------
/app/components/Button/tests/index.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Testing our Button component
3 | */
4 |
5 | import React from 'react';
6 | import { mount } from 'enzyme';
7 |
8 | import Button from '../index';
9 |
10 | const handleRoute = () => {};
11 | const href = 'http://mxstbr.com';
12 | const children = Test ;
13 | const renderComponent = (props = {}) =>
14 | mount(
15 |
16 | {children}
17 | ,
18 | );
19 |
20 | describe(' ', () => {
21 | it('should render an tag if no route is specified', () => {
22 | const renderedComponent = renderComponent({ href });
23 | expect(renderedComponent.find('a').length).toEqual(1);
24 | });
25 |
26 | it('should render a tag to change route if the handleRoute prop is specified', () => {
27 | const renderedComponent = renderComponent({ handleRoute });
28 | expect(renderedComponent.find('button').length).toEqual(1);
29 | });
30 |
31 | it('should have children', () => {
32 | const renderedComponent = renderComponent();
33 | expect(renderedComponent.contains(children)).toEqual(true);
34 | });
35 |
36 | it('should handle click events', () => {
37 | const onClickSpy = jest.fn();
38 | const renderedComponent = renderComponent({ onClick: onClickSpy });
39 | renderedComponent.find('a').simulate('click');
40 | expect(onClickSpy).toHaveBeenCalled();
41 | });
42 |
43 | it('should have a className attribute', () => {
44 | const renderedComponent = renderComponent();
45 | expect(renderedComponent.find('a').prop('className')).toBeDefined();
46 | });
47 |
48 | it('should not adopt a type attribute when rendering an tag', () => {
49 | const type = 'text/html';
50 | const renderedComponent = renderComponent({ href, type });
51 | expect(renderedComponent.find('a').prop('type')).toBeUndefined();
52 | });
53 |
54 | it('should not adopt a type attribute when rendering a button', () => {
55 | const type = 'submit';
56 | const renderedComponent = renderComponent({ handleRoute, type });
57 | expect(renderedComponent.find('button').prop('type')).toBeUndefined();
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/app/containers/LocaleToggle/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { browserHistory } from 'react-router-dom';
4 | import { shallow, mount } from 'enzyme';
5 |
6 | import LocaleToggle, { mapDispatchToProps } from '../index';
7 | import { changeLocale } from '../../LanguageProvider/actions';
8 | import LanguageProvider from '../../LanguageProvider';
9 |
10 | import configureStore from '../../../configureStore';
11 | import { translationMessages } from '../../../i18n';
12 |
13 | describe(' ', () => {
14 | let store;
15 |
16 | beforeAll(() => {
17 | store = configureStore({}, browserHistory);
18 | });
19 |
20 | it('should render the default language messages', () => {
21 | const renderedComponent = shallow(
22 |
23 |
24 |
25 |
26 | ,
27 | );
28 | expect(renderedComponent.contains( )).toBe(true);
29 | });
30 |
31 | it('should present the default `en` english language option', () => {
32 | const renderedComponent = mount(
33 |
34 |
35 |
36 |
37 | ,
38 | );
39 | expect(renderedComponent.contains(en )).toBe(
40 | true,
41 | );
42 | });
43 |
44 | describe('mapDispatchToProps', () => {
45 | describe('onLocaleToggle', () => {
46 | it('should be injected', () => {
47 | const dispatch = jest.fn();
48 | const result = mapDispatchToProps(dispatch);
49 | expect(result.onLocaleToggle).toBeDefined();
50 | });
51 |
52 | it('should dispatch changeLocale when called', () => {
53 | const dispatch = jest.fn();
54 | const result = mapDispatchToProps(dispatch);
55 | const locale = 'de';
56 | const evt = { target: { value: locale } };
57 | result.onLocaleToggle(evt);
58 | expect(dispatch).toHaveBeenCalledWith(changeLocale(locale));
59 | });
60 | });
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/app/containers/RepoListItem/tests/index.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test the repo list item
3 | */
4 |
5 | import React from 'react';
6 | import { shallow, render } from 'enzyme';
7 | import { IntlProvider } from 'react-intl';
8 |
9 | import ListItem from 'components/ListItem';
10 | import { RepoListItem } from '../index';
11 |
12 | const renderComponent = (props = {}) =>
13 | render(
14 |
15 |
16 | ,
17 | );
18 |
19 | describe(' ', () => {
20 | let item;
21 |
22 | // Before each test reset the item data for safety
23 | beforeEach(() => {
24 | item = {
25 | owner: {
26 | login: 'mxstbr',
27 | },
28 | html_url: 'https://github.com/react-boilerplate/react-boilerplate',
29 | name: 'react-boilerplate',
30 | open_issues_count: 20,
31 | full_name: 'react-boilerplate/react-boilerplate',
32 | };
33 | });
34 |
35 | it('should render a ListItem', () => {
36 | const renderedComponent = shallow( );
37 | expect(renderedComponent.find(ListItem).length).toBe(1);
38 | });
39 |
40 | it('should not render the current username', () => {
41 | const renderedComponent = renderComponent({
42 | item,
43 | currentUser: item.owner.login,
44 | });
45 | expect(renderedComponent.text()).not.toContain(item.owner.login);
46 | });
47 |
48 | it('should render usernames that are not the current one', () => {
49 | const renderedComponent = renderComponent({
50 | item,
51 | currentUser: 'nikgraf',
52 | });
53 | expect(renderedComponent.text()).toContain(item.owner.login);
54 | });
55 |
56 | it('should render the repo name', () => {
57 | const renderedComponent = renderComponent({ item });
58 | expect(renderedComponent.text()).toContain(item.name);
59 | });
60 |
61 | it('should render the issue count', () => {
62 | const renderedComponent = renderComponent({ item });
63 | expect(renderedComponent.text()).toContain(item.open_issues_count);
64 | });
65 |
66 | it('should render the IssueIcon', () => {
67 | const renderedComponent = renderComponent({ item });
68 | expect(renderedComponent.find('svg').length).toBe(1);
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/app/utils/tests/request.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test the request function
3 | */
4 |
5 | import request from '../request';
6 |
7 | describe('request', () => {
8 | // Before each test, stub the fetch function
9 | beforeEach(() => {
10 | window.fetch = jest.fn();
11 | });
12 |
13 | describe('stubbing successful response', () => {
14 | // Before each test, pretend we got a successful response
15 | beforeEach(() => {
16 | const res = new Response('{"hello":"world"}', {
17 | status: 200,
18 | headers: {
19 | 'Content-type': 'application/json',
20 | },
21 | });
22 |
23 | window.fetch.mockReturnValue(Promise.resolve(res));
24 | });
25 |
26 | it('should format the response correctly', done => {
27 | request('/thisurliscorrect')
28 | .catch(done)
29 | .then(json => {
30 | expect(json.hello).toBe('world');
31 | done();
32 | });
33 | });
34 | });
35 |
36 | describe('stubbing 204 response', () => {
37 | // Before each test, pretend we got a successful response
38 | beforeEach(() => {
39 | const res = new Response('', {
40 | status: 204,
41 | statusText: 'No Content',
42 | });
43 |
44 | window.fetch.mockReturnValue(Promise.resolve(res));
45 | });
46 |
47 | it('should return null on 204 response', done => {
48 | request('/thisurliscorrect')
49 | .catch(done)
50 | .then(json => {
51 | expect(json).toBeNull();
52 | done();
53 | });
54 | });
55 | });
56 |
57 | describe('stubbing error response', () => {
58 | // Before each test, pretend we got an unsuccessful response
59 | beforeEach(() => {
60 | const res = new Response('', {
61 | status: 404,
62 | statusText: 'Not Found',
63 | headers: {
64 | 'Content-type': 'application/json',
65 | },
66 | });
67 |
68 | window.fetch.mockReturnValue(Promise.resolve(res));
69 | });
70 |
71 | it('should catch errors', done => {
72 | request('/thisdoesntexist').catch(err => {
73 | expect(err.response.status).toBe(404);
74 | expect(err.response.statusText).toBe('Not Found');
75 | done();
76 | });
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------