Skip to content

Commit

Permalink
feature(notifications): get notifications
Browse files Browse the repository at this point in the history
- get all user notifications
- implement tests
- improve previous test coverage
  • Loading branch information
kevpy committed Apr 26, 2019
1 parent ce2eb62 commit 92bfd2e
Show file tree
Hide file tree
Showing 14 changed files with 589 additions and 72 deletions.
1 change: 1 addition & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import loginAction from './login.actions';

export * from './notifications.actions';
export default loginAction;
export * from './register';
export * from './profile';
Expand Down
2 changes: 1 addition & 1 deletion src/actions/login.actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ const loginAction = (email, password) => async (dispatch) => {
})
.then(res => res.data)
.then((user) => {
dispatch(success(user));
localStorage.setItem('user', JSON.stringify(user));
dispatch(success(user));

// return user;
})
Expand Down
17 changes: 17 additions & 0 deletions src/actions/notifications.actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import axios from 'axios';
import axiosHeader from '../axios_config';
import { notificationConstants } from '../constants';

export const getNotifications = () => async (dispatch) => {
await axios
.get(`${process.env.REACT_APP_BASE_URL}/profile/view_profile/notifications/`, axiosHeader)
.then((result) => {
dispatch({
type: notificationConstants.NOTIFY_SUCCESS,
payload: result.data,
});
})
.catch(() => {});
};

export default getNotifications;
13 changes: 5 additions & 8 deletions src/components/Login/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,11 @@ export class Login extends Component {
e.preventDefault();
const { name, value } = e.target;
const { formErrors } = this.state;
switch (name) {
case 'email':
formErrors.email = emailRegex.test(value)
? ''
: 'invalid email address';
break;
default:
break;
if (name === 'email') {
formErrors.email = emailRegex.test(value)
? ''
: 'invalid email address';
} else {
}

this.setState({ formErrors, [name]: value });
Expand Down
167 changes: 136 additions & 31 deletions src/components/Login/Login.test.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,29 @@
import React from 'react';
import { combineReducers, applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import moxios from 'moxios';
import thunk from 'redux-thunk';
import Enzyme, { shallow, mount } from 'enzyme';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import loginReducer from '../../reducers/index';
import loginAction from '../../actions/index';
import { Login } from './Login';
import store from '../../store/store';
import { loginConstants } from '../../constants';
import loginReducer from '../../reducers/login.reducer';

Enzyme.configure({ adapter: new Adapter() });

const wrongEmail = 'test@email.com';
const wrongPassword = 'passrd12';

const email = 'user@email.com';
const password = 'passw0rd';

const allReducer = combineReducers({ login: loginReducer });
const testStore = createStore(allReducer, applyMiddleware(thunk));
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe('component', () => {
const wrapper = shallow(
<Provider store={store}>
<Login />
</Provider>,
);
const wrapper = shallow(<Login />);
const props = { user: { errors: {} }, loginAction: jest.fn() };
const loginComponent = shallow(<Login {...props} />);

const emailInput = wrapper.find('#email');

it('one button present initially', () => {
expect(wrapper.find('button').length).toEqual(0);
expect(wrapper.find('button').length).toEqual(1);
});

it('present', () => {
Expand Down Expand Up @@ -66,28 +57,142 @@ describe('component', () => {
'invalid email address',
);
});

it('should redirect', () => {
wrapper.setState({
show: false,
close: true,
notifications: {},
});

const newProps = {
user: {
errors: '',
},
history: {
push: jest.fn(),
},
loginAction: jest.fn(),
};

const spy = jest.spyOn(wrapper.instance(), 'componentWillReceiveProps');
wrapper.instance().componentWillReceiveProps(newProps);
expect(spy).toBeCalledWith(newProps);
});

it('should close alert', () => {
wrapper.setState({
show: true,
});

const spy = jest.spyOn(wrapper.instance(), 'onAlertClose');
wrapper.instance().onAlertClose();
expect(spy).toHaveBeenCalled();
});
});

describe('actions', () => {
beforeEach(() => {
jest.setTimeout(10000);
});

it('Should show Login errors', async () => {
const expectedErrors = 'Invalid email or password provided.';
const expectedAction = {
errors: expectedErrors,
it('creates NOTIFY_SUCCESS after successfully fetching notifications', () => {
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: {
user: {},
},
});
});

const expectedActions = [
{
type: loginConstants.LOGIN_SUCCESS,
payload: {},
},
];

const store = mockStore({
payload: {},
});

return store.dispatch(loginAction()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});

it('it dispatches login failure', async () => {
const errResp = {
status: 400,
response: { message: 'problem' },
};
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.reject(errResp);
});

const expectedActions = [
{
type: loginConstants.LOGIN_FAILURE,
payload: {},
},
];

const store = mockStore({
payload: {},
});

return store.dispatch(loginAction()).catch(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
});

describe('Test reducer', () => {
const initialState = {
loggedIn: false,
user: {},
};

it('return initial state if no action', () => {
expect(loginReducer(initialState, {})).toEqual({
loggedIn: false,
user: {},
};
await testStore.dispatch(loginAction(wrongEmail, wrongPassword));
setTimeout(() => {
expect(testStore.getState().login.login).resolves.toEqual(expectedAction);
}, 3000);
});
});

it('return LOGIN_SUCCESS', () => {
expect(
loginReducer(initialState, {
type: loginConstants.LOGIN_SUCCESS,
payload: {
user: {},
},
}),
).toEqual({
loggedIn: true,
user: {
user: {},
},
errors: '',
});
});
it('Should show logged in true', async () => {
const isLoggedIn = true;
await testStore.dispatch(loginAction(email, password));
expect(testStore.getState().login.login.loggedIn).toEqual(isLoggedIn);

it('return LOGIN_FAILURE', () => {
const errors = 'Invalid email or password';
expect(
loginReducer(initialState, {
type: loginConstants.LOGIN_FAILURE,
error: errors,
}),
).toEqual({
loggedIn: false,
user: {},
errors,
});
});
});
127 changes: 127 additions & 0 deletions src/components/NotificationBadge/NotificationBadge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Overlay, Popover } from 'react-bootstrap';
import './NotificationBadge.scss';

export class NotificationBadgeComponent extends Component {
constructor(props) {
super(props);

this.state = {
show: false,
close: true,
notifications: null,
};

this.handleClick = ({ target }) => {
const { close } = this.state;
if (close) {
this.setState(newState => ({ target, show: !newState.show }));
this.state.close = false;
}
};

this.handleClose = this.handleClose.bind(this);
}

componentWillReceiveProps(nextProps) {
if (Object.keys(nextProps.notify).length !== 0) {
this.setState({
notifications: nextProps.notify,
});
}
}

handleClose() {
this.setState(newState => ({ close: !newState.close }));
this.setState({
show: false,
});
}

render() {
const { show, target, notifications } = this.state;
// console.log(notifications);

return (
<div className="mr-3">
{notifications ? (
<i className="fa fa-bell fa-1x icon-blue" onClick={this.handleClick}>
<span className="badge-1">{Object.keys(notifications).length}</span>
<Overlay
show={show}
target={target}
placement="bottom"
container={this}
containerPadding={20}
>
<Popover id="popover-contained" title="Notifications">
<div className="content">
{Object.keys(notifications).map(obj => (
<div>
<li key={obj}><a href={`article/${notifications[obj].slug}`}>{notifications[obj].notification}</a></li>
<hr />
</div>
))}
</div>
<div className="popover-footer">
<button
type="button"
className="b btn-info"
onClick={this.handleClose}
>
close
</button>
<button
type="button"
className="b btn-warning"
onClick={this.handleClose}
disabled
>
mark all as read
</button>
</div>
</Popover>
</Overlay>
</i>
) : (
<i className="fa fa-bell fa-2x icon-blue" onClick={this.handleClick}>
<Overlay
show={show}
target={target}
placement="bottom"
container={this}
containerPadding={20}
>
<Popover id="popover-contained" title="Notifications">
<div className="content">
<li>You have no notifications</li>
</div>
<div className="popover-footer">
<button
type="button"
className="close-notification"
onClick={this.handleClose}
>
close
</button>
</div>
</Popover>
</Overlay>
</i>
)}
</div>
);
}
}

const mapStateToProps = state => ({
notify: state.notifications.notifications.notifications,
});

const NotificationBadge = connect(
mapStateToProps,
null,
)(NotificationBadgeComponent);

export default NotificationBadge;
Loading

0 comments on commit 92bfd2e

Please sign in to comment.