Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Fix offline behaviour - Closes #545 #612

Merged
merged 9 commits into from
Aug 21, 2017
44 changes: 24 additions & 20 deletions src/components/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,32 @@ import Dialog from '../dialog';
import Toaster from '../toaster';
import Tabs from '../tabs';
import LoadingBar from '../loadingBar';
import OfflineWrapper from '../offlineWrapper';
import offlineStyle from '../offlineWrapper/offlineWrapper.css';

const App = () => (
<section className={`${grid.row} ${styles['body-wrapper']}`}>
<div className={`${grid['col-xs-12']} ${grid['col-sm-12']} ${grid['col-md-10']} ${grid['col-md-offset-1']}`}>
<Header />
<main>
<PrivateRoutes path='/main' render={ ({ match }) => (
<main>
<Account />
<Tabs />
<Route path={`${match.url}/transactions`} component={Transactions} />
<Route path={`${match.url}/voting`} component={Voting} />
<Route path={`${match.url}/forging`} component={Forging} />
</main>
)} />
<Route exact path="/" component={Login} />
</main>
<Dialog />
<Toaster />
<LoadingBar />
</div>
</section>
<OfflineWrapper>
<section className={`${grid.row} ${styles['body-wrapper']}`}>
<div className={`${grid['col-xs-12']} ${grid['col-sm-12']} ${grid['col-md-10']} ${grid['col-md-offset-1']}`}>
<Header />
<main>
<PrivateRoutes path='/main' render={ ({ match }) => (
<main className={offlineStyle.disableWhenOffline}>
<Account />
<Tabs />
<Route path={`${match.url}/transactions`} component={Transactions} />
<Route path={`${match.url}/voting`} component={Voting} />
<Route path={`${match.url}/forging`} component={Forging} />
</main>
)} />
<Route exact path="/" component={Login} />
</main>
<Dialog />
<Toaster />
<LoadingBar />
</div>
</section>
</OfflineWrapper>
);

export default App;
17 changes: 9 additions & 8 deletions src/components/header/headerElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import RegisterDelegate from '../registerDelegate';
import Send from '../send';
import PrivateWrapper from '../privateWrapper';
import SecondPassphraseMenu from '../secondPassphrase';
import offlineStyle from '../offlineWrapper/offlineWrapper.css';

const HeaderElement = props => (
<header className={`${grid.row} ${grid['between-xs']} ${styles.wrapper}`} >
Expand All @@ -18,7 +19,7 @@ const HeaderElement = props => (
</div>
<PrivateWrapper>
<IconMenu
className={`${styles.iconButton} main-menu-icon-button`}
className={`${styles.iconButton} main-menu-icon-button ${offlineStyle.disableWhenOffline}`}
icon="more_vert"
position="topRight"
menuRipple
Expand All @@ -27,12 +28,12 @@ const HeaderElement = props => (
{
!props.account.isDelegate &&
<MenuItem caption="Register as delegate"
className='register-as-delegate'
onClick={() => props.setActiveDialog({
title: 'Register as delegate',
childComponent: RegisterDelegate,
})}
/>
className='register-as-delegate'
onClick={() => props.setActiveDialog({
title: 'Register as delegate',
childComponent: RegisterDelegate,
})}
/>
}
<SecondPassphraseMenu />
<MenuItem caption="Sign message"
Expand All @@ -54,7 +55,7 @@ const HeaderElement = props => (
/>
</IconMenu>
<Button className={`${styles.button} logout-button`} raised onClick={props.logOut}>logout</Button>
<Button className={`${styles.button} send-button`}
<Button className={`${styles.button} send-button ${offlineStyle.disableWhenOffline}`}
raised primary
onClick={() => props.setActiveDialog({
title: 'Send',
Expand Down
16 changes: 16 additions & 0 deletions src/components/offlineWrapper/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { connect } from 'react-redux';
import styles from './offlineWrapper.css';

export const OfflineWrapperComponent = props => (
<span className={props.offline && styles.isOffline}>
{ props.children }
</span>
);


const mapStateToProps = state => ({
offline: state.loading && state.loading.indexOf('offline') > -1,
});

export default connect(mapStateToProps)(OfflineWrapperComponent);
42 changes: 42 additions & 0 deletions src/components/offlineWrapper/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { expect } from 'chai';
import { mount, shallow } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import OfflineWrapper, { OfflineWrapperComponent } from './index';
import styles from './offlineWrapper.css';

const fakeStore = configureStore();

describe('OfflineWrapperComponent', () => {
it('renders props.children inside a span with "offline" class if props.offline', () => {
const wrapper = shallow(
<OfflineWrapperComponent offline={true}><h1 /> </OfflineWrapperComponent>);
expect(wrapper).to.contain(<h1 />);
expect(wrapper).to.have.className(styles.isOffline);
});

it('renders without "offline" class if props.offline', () => {
const wrapper = shallow(
<OfflineWrapperComponent offline={false}><h1 /> </OfflineWrapperComponent>);
expect(wrapper).not.to.have.className(styles.isOffline);
});
});

describe('OfflineWrapper', () => {
it('should set props.offline = false if "offline" is not in store.loading', () => {
const store = fakeStore({
loading: [],
});
const wrapper = mount(<Provider store={store}><OfflineWrapper /></Provider>);
expect(wrapper.find(OfflineWrapperComponent).props().offline).to.equal(false);
});

it('should set props.offline = true if "offline" is in store.loading', () => {
const store = fakeStore({
loading: ['offline'],
});
const wrapper = mount(<Provider store={store}><OfflineWrapper /></Provider>);
expect(wrapper.find(OfflineWrapperComponent).props().offline).to.equal(true);
});
});
4 changes: 4 additions & 0 deletions src/components/offlineWrapper/offlineWrapper.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.isOffline .disableWhenOffline {
opacity: 0.5;
pointer-events: none;
}
2 changes: 2 additions & 0 deletions src/store/middlewares/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import metronomeMiddleware from './metronome';
import accountMiddleware from './account';
import loginMiddleware from './login';
import offlineMiddleware from './offline';
import notificationMiddleware from './notification';

export default [
loginMiddleware,
metronomeMiddleware,
accountMiddleware,
offlineMiddleware,
notificationMiddleware,
];
27 changes: 27 additions & 0 deletions src/store/middlewares/offline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import actionsType from '../../constants/actions';
import { successToastDisplayed, errorToastDisplayed } from '../../actions/toaster';
import { loadingStarted, loadingFinished } from '../../utils/loading';

const offlineMiddleware = store => next => (action) => {
const state = store.getState();
switch (action.type) {
case actionsType.activePeerUpdate:
if (action.data.online === false && state.peers.status.online === true) {
const address = `${state.peers.data.currentPeer}:${state.peers.data.port}`;
store.dispatch(errorToastDisplayed({ label: `Failed to connect to node ${address}` }));
loadingStarted('offline');
} else if (action.data.online === true && state.peers.status.online === false) {
store.dispatch(successToastDisplayed({ label: 'Connection re-established' }));
loadingFinished('offline');
}
if (action.data.online !== state.peers.status.online) {
next(action);
}
break;
default:
next(action);
break;
}
};

export default offlineMiddleware;
77 changes: 77 additions & 0 deletions src/store/middlewares/offline.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect } from 'chai';
import { spy, stub } from 'sinon';
import middleware from './offline';
import { successToastDisplayed, errorToastDisplayed } from '../../actions/toaster';
import actionType from '../../constants/actions';


describe('Offline middleware', () => {
let store;
let next;
let action;
let peers;

beforeEach(() => {
store = stub();
store.dispatch = spy();
next = spy();
action = {
type: actionType.activePeerUpdate,
data: {},
};
peers = {
data: {
port: 4000,
currentPeer: 'localhost',
},
status: {},
};
store.getState = () => ({ peers });
});

it('should pass the action to next middleware on some random action', () => {
const randomAction = {
type: 'TEST_ACTION',
};

middleware(store)(next)(randomAction);
expect(next).to.have.been.calledWith(randomAction);
});

it(`should dispatch errorToastDisplayed on ${actionType.activePeerUpdate} action if !action.data.online and state.peer.status.online`, () => {
peers.status.online = true;
action.data.online = false;

middleware(store)(next)(action);
expect(store.dispatch).to.have.been.calledWith(errorToastDisplayed({
label: `Failed to connect to node ${peers.data.currentPeer}:${peers.data.port}`,
}));
});

it(`should dispatch successToastDisplayed on ${actionType.activePeerUpdate} action if action.data.online and !state.peer.status.online`, () => {
peers.status.online = false;
action.data.online = true;

middleware(store)(next)(action);
expect(store.dispatch).to.have.been.calledWith(successToastDisplayed({
label: 'Connection re-established',
}));
});

it(`should not call next() on ${actionType.activePeerUpdate} action if action.data.online === state.peer.status.online`, () => {
peers.status.online = false;
action.data.online = false;

middleware(store)(next)(action);
expect(next).not.to.have.been.calledWith();
});

it(`should call next() on ${actionType.activePeerUpdate} action if action.data.online !== state.peer.status.online`, () => {
peers.status.online = true;
action.data.online = false;

middleware(store)(next)(action);
expect(next).to.have.been.calledWith(action);
});
});

2 changes: 1 addition & 1 deletion src/store/reducers/peers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import actionTypes from '../../constants/actions';
*
* @returns {Object} - Next state object
*/
const peers = (state = {}, action) => {
const peers = (state = { status: {} }, action) => {
switch (action.type) {
case actionTypes.activePeerSet:
return Object.assign({}, state, { data: action.data });
Expand Down