Project

General

Profile

« Previous | Next » 

Revision 1c40f7e8

Added by Gail Steiger over 7 years ago

fixes #18010 - UI Notifications front-end implementation

View differences:

app/assets/stylesheets/patternfly_and_overrides.scss
}
}
}
// gridster on dashboard conflicts with notifications drawer
.gridster .gs-w {
z-index: auto !important;
}
// workaround to patternfly css after removing class navbar-pf from navbar
// to retain original navbar height
.navbar-outer .drawer-pf {
height: calc(100vh - 46px);
top: 26px;
}
app/views/home/_topbar.html.erb
<%= render_menu :header_menu %>
<%= render 'home/user_dropdown' if SETTINGS[:login] %>
</ul>
<% if SETTINGS[:login] %>
<div id='notification_drawer'> </div>
<%= mount_react_component('NotificationDrawer', '#notification_drawer', {}) %>
<% end %>
</div>
<% end %>
</div>
app/views/home/_user_dropdown.html.erb
<li id='notification_icon' class="drawer-pf-trigger dropdown"> </li>
<%= mount_react_component('NotificationDrawerToggle', '#notification_icon', {:url => notification_recipients_path}.to_json ) %>
<li class="dropdown menu_tab_dropdown">
<%= user_header %>
<ul class="dropdown-menu pull-right">
package.json
"jquery.cookie": "~1.4.1",
"jstz": "~1.0.7",
"lodash": "~4.15.0",
"moment": "^2.17.1",
"multiselect": "~0.9.12",
"react": "^15.1.0",
"react-bootstrap": "^0.30.0",
......
"automock": true,
"verbose": true,
"collectCoverage": true,
"collectCoverageFrom": ["webpack/**/*.js", "!webpack/**/bundle*"],
"coverageReporters": ["lcov"],
"collectCoverageFrom": [
"webpack/**/*.js",
"!webpack/**/bundle*"
],
"coverageReporters": [
"lcov"
],
"unmockedModulePathPatterns": [
"react",
"node_modules/"
webpack/assets/javascripts/react_app/API.js
.error((jqXHR, textStatus, errorThrown) => {
ServerActions.hostsRequestError(jqXHR, textStatus, errorThrown);
});
},
getNotifications(url) {
$.get(url)
.success(
(response, textStatus, jqXHR) => {
ServerActions.receivedNotifications(response, textStatus, jqXHR);
})
.error((jqXHR, textStatus, errorThrown) => {
ServerActions.notificationsRequestError(jqXHR, textStatus, errorThrown);
});
},
markNotificationAsRead(url) {
const data = JSON.stringify({'seen': true});
$.ajax({
url: url,
contentType: 'application/json',
type: 'put',
dataType: 'json',
data: data,
success: function (response, textstatus, jqXHR) {
ServerActions.notificationMarkedAsRead(response, textstatus, jqXHR);
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR);
}
});
}
};
webpack/assets/javascripts/react_app/actions/NotificationActions.js
import API from '../API';
import AppDispatcher from '../dispatcher';
import {ACTIONS} from '../constants';
const TIMER = 10000;
export default {
getNotifications(url) {
if (document.visibilityState === 'visible') {
API.getNotifications(url);
}
setTimeout(() => {
this.getNotifications(url);
}, TIMER);
},
toggleNotificationDrawer() {
AppDispatcher.dispatch({
actionType: ACTIONS.NOTIFICATIONS_DRAWER_TOGGLE
});
},
expandDrawerTab(group) {
AppDispatcher.dispatch({
actionType: ACTIONS.NOTIFICATIONS_EXPAND_DRAWER_TAB,
expand: group
});
},
markAsRead(url) {
API.markNotificationAsRead(url);
}
};
webpack/assets/javascripts/react_app/actions/ServerActions.js
errorThrown: errorThrown
}
});
},
receivedNotifications(response, textStatus, jqXHR) {
AppDispatcher.dispatch({
actionType: ACTIONS.RECEIVED_NOTIFICATIONS,
notifications: response.notifications
});
},
notificationsRequestError(jqXHR, textStatus, errorThrown) {
AppDispatcher.dispatch({
actionType: ACTIONS.NOTIFICATIONSS_REQUEST_ERROR, info: {
jqXHR: jqXHR,
textStatus: textStatus,
errorThrown: errorThrown
}
});
},
notificationMarkedAsRead(response, textStatus, jqXHR) {
AppDispatcher.dispatch(({
actionType: ACTIONS.NOTIFICATIONS_MARKED_AS_READ
}));
}
};
webpack/assets/javascripts/react_app/common/MountingService.js
import React from 'react';
import StatisticsChartsList from '../components/charts/StatisticsChartsList';
import PowerStatusContainer from '../components/hosts/PowerStatusContainer';
import NotificationDrawerToggle from '../components/notifications/NotificationDrawerToggle';
import NotificationDrawer from '../components/notifications/NotificationDrawer';
import ReactDOM from 'react-dom';
export function mount(component, selector, data) {
......
PowerStatusContainer: {
type: PowerStatusContainer,
markup: <PowerStatusContainer url={data.url} id={data.id}/>
},
NotificationDrawerToggle: {
type: NotificationDrawerToggle,
markup: <NotificationDrawerToggle url={data.url}/>
},
NotificationDrawer: {
type: NotificationDrawer,
markup: <NotificationDrawer data={data} />
}
};
webpack/assets/javascripts/react_app/common/commonStyles.css
.pointer {
cursor: pointer;
}
webpack/assets/javascripts/react_app/common/testHelpers.js
export default {
mockStorage: () => {
let storage = {};
return {
setItem: (key, value) => {
storage[key] = value || '';
},
getItem: (key) => {
return key in storage ? storage[key] : null;
},
removeItem: (key) => {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key: (i) => {
let keys = Object.keys(storage);
return keys[i] || null;
}
};
}
};
webpack/assets/javascripts/react_app/components/common/Icon.js
import React from 'react';
import {ICON_CSS} from '../../constants';
const Icon = (props) => {
const classNames = props.css ? ICON_CSS[props.type] + ' ' + props.css : ICON_CSS[props.type];
return (
<span className={classNames}></span>
);
};
export default Icon;
webpack/assets/javascripts/react_app/components/common/Icon.test.js
jest.unmock('./Icon');
import React from 'react';
import { shallow } from 'enzyme';
import Icon from './Icon';
describe('Icon', () => {
it('displays icon css', () => {
const wrapper = shallow(<Icon type="ok" />);
expect(wrapper.html()).toEqual('<span class="pficon pficon-ok"></span>');
});
it('can receive additionl css classes', () => {
const wrapper = shallow(<Icon type="ok" css="pull-left" />);
expect(wrapper.html()).toEqual('<span class="pficon pficon-ok pull-left"></span>');
});
});
webpack/assets/javascripts/react_app/components/notifications/Notification.js
import React from 'react';
import Icon from '../common/Icon';
import moment from 'moment';
import NotificationActions from '../../actions/NotificationActions';
import '../../common/commonStyles.css';
/* eslint-disable camelcase */
const Notification = ({created_at, seen, text, level, id}) => {
const created = moment(created_at);
const title = __('Click to mark as read').toString();
const tooltip = {
title: title,
'data-toggle': 'tooltip',
'data-placement': 'top'
};
const markup = seen ?
(<span className="drawer-pf-notification-message">{text}</span>) :
(<strong {...tooltip} className="drawer-pf-notification-message pointer"
onClick={markAsRead}>{text}</strong>);
function markAsRead() {
NotificationActions.markAsRead('/notification_recipients/' + id);
}
window.tfm.tools.activateTooltips();
return (
<div className="drawer-pf-notification">
<Icon type={level} css="pull-left"></Icon>
{markup}
<div className="drawer-pf-notification-info">
<span className="date">{created.format('M/D/YY')}</span>
<span className="time">{created.format('hh:mm:ss A')}</span>
</div>
</div>
);
};
export default Notification;
webpack/assets/javascripts/react_app/components/notifications/Notification.test.js
jest.unmock('./Notification');
import React from 'react';
import { mount } from 'enzyme';
import Notification from './Notification';
/* eslint-disable camelcase */
const notification = {
id: 1,
text: 'Job well done',
level: 'success',
created_at: '2016-12-13 16:52:47Z'
};
/* eslint-enable camelcase */
function setup(notification) {
return mount(<Notification {...notification} />);
}
describe('Notification', () => {
beforeEach(() => {
global.__ = (text) => text;
global.tfm = {
tools: {
activateTooltips: () => {}
}
};
});
it('displays text', () => {
const wrapper = setup(notification);
const messageElement = wrapper.find('.drawer-pf-notification-message');
expect(messageElement.text()).toBe('Job well done');
});
it('displays icon', () => {
const wrapper = setup(notification);
const iconElement = wrapper.find('.pficon.pficon-ok.pull-left');
expect(iconElement.length).toBe(1);
});
it('displays created date', () => {
const wrapper = setup(notification);
const dateElement = wrapper.find('.date');
expect(dateElement.text()).toBe('12/13/16');
});
xit('displays created time', () => {
const wrapper = setup(notification);
const timeElement = wrapper.find('.time');
expect(timeElement.text()).toBe('06:52:47 PM');
});
});
webpack/assets/javascripts/react_app/components/notifications/NotificationAccordion.js
import React from 'react';
import helpers from '../../common/helpers';
import NotificationPanel from './NotificationPanel';
import NotificationsStore from '../../stores/NotificationsStore';
import { ACTIONS } from '../../constants';
class NotificationAccordion extends React.Component {
constructor(props) {
super(props);
helpers.bindMethods(this, ['onChange']);
this.state = {expandedGroup: NotificationsStore.getExpandedGroup()};
}
componentDidMount() {
NotificationsStore.addChangeListener(this.onChange);
}
onChange(actionType) {
switch (actionType) {
case ACTIONS.NOTIFICATIONS_EXPAND_DRAWER_TAB: {
const expandedGroup = NotificationsStore.getExpandedGroup();
this.setState({ expandedGroup: expandedGroup });
break;
}
default:
break;
}
}
render() {
const notifications = this.props.notifications;
const keys = Object.keys(notifications);
let markup = keys.map((key) => {
return (
<NotificationPanel key={key} title={key} id={key} expandedGroup={this.state.expandedGroup}
notifications={this.props.notifications[key]} />
);
});
return (
<div className="panel-group" id="notification-drawer-accordion">
{markup}
</div>
);
}
}
export default NotificationAccordion;
webpack/assets/javascripts/react_app/components/notifications/NotificationAccordion.test.js
jest.unmock('./NotificationAccordion');
jest.unmock('../../stores/NotificationsStore');
import React from 'react';
import { shallow } from 'enzyme';
import NotificationAccordion from './NotificationAccordion';
import testData from '../../stores/NotificationsTestData';
import NotificationsStore from '../../stores/NotificationsStore';
function setup(data) {
return shallow(<NotificationAccordion notifications={data}/>);
}
describe('NotificationAccordion', () => {
it('runs a test', () => {
const data = NotificationsStore.prepareNotifications(testData);
// eslint-disable-next-line no-unused-vars
const wrapper = setup(data);
expect('not implemented').toBeTruthy();
});
});
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawer.js
import React, { Component } from 'react';
import helpers from '../../common/helpers';
import NotificationsStore from '../../stores/NotificationsStore';
import NotificationDrawerTitle from './NotificationDrawerTitle';
import NotificationAccordion from './NotificationAccordion';
import {ACTIONS} from '../../constants';
class NotificationDrawer extends Component {
constructor(props) {
super(props);
this.state = {
notifications: NotificationsStore.getNotifications(),
drawerOpen: NotificationsStore.getIsDrawerOpen()
};
helpers.bindMethods(this, ['onChange']);
}
componentDidMount() {
NotificationsStore.addChangeListener(this.onChange);
}
onChange(actionType) {
switch (actionType) {
case ACTIONS.RECEIVED_NOTIFICATIONS: {
this.setState({ notifications: NotificationsStore.getNotifications() });
break;
}
case ACTIONS.NOTIFICATIONS_DRAWER_TOGGLE: {
const isOpen = NotificationsStore.getIsDrawerOpen();
this.setState({
drawerOpen: isOpen
});
break;
}
default:
break;
}
}
render() {
// render title and accordion
const toggleClass = this.state.drawerOpen ? '' : ' hide';
return (
<div className={'drawer-pf drawer-pf-notifications-non-clickable' + toggleClass}>
<NotificationDrawerTitle text="Notifications" />
<NotificationAccordion notifications={this.state.notifications} />
</div>
);
}
}
export default NotificationDrawer;
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawer.test.js
jest.unmock('./NotificationDrawer');
import React from 'react';
import { shallow } from 'enzyme';
import NotificationDrawer from './NotificationDrawer';
import testHelpers from '../../common/testHelpers';
function setup() {
return shallow(<NotificationDrawer />);
}
describe('NotificationDrawer', () => {
beforeEach(() => {
global.sessionStorage = testHelpers.mockStorage();
});
it('runs a test', () => {
setup();
expect('not implemented').toBeTruthy();
});
});
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawerTitle.js
import React from 'react';
const NotificationDrawerTitle = ({text}) => (
<div className="drawer-pf-title">
<a className="drawer-pf-toggle-expand"></a>
<h3 className="text-center">{text}</h3>
</div>
);
export default NotificationDrawerTitle;
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawerTitle.test.js
jest.unmock('./NotificationDrawerTitle');
import React from 'react';
import { shallow } from 'enzyme';
import NotificationDrawerTitle from './NotificationDrawerTitle';
function setup(title) {
return shallow(<NotificationDrawerTitle text={title} />);
}
describe('NotificationDrawerTitle', () => {
it('displays title', () => {
const wrapper = setup('Drawer Title');
const title = wrapper.find('h3');
expect(title.text()).toBe('Drawer Title');
});
});
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawerToggle.js
import React, { Component } from 'react';
import helpers from '../../common/helpers';
import NotificationsStore from '../../stores/NotificationsStore';
import NotificationActions from '../../actions/NotificationActions';
import { ACTIONS } from '../../constants';
class NotificationDrawerToggle extends Component {
constructor(props) {
super(props);
this.state = { count: 0, isLoaded: false, drawerOpen: false };
helpers.bindMethods(this, ['onChange', 'onClick']);
}
componentDidMount() {
NotificationsStore.addChangeListener(this.onChange);
// NotificationsStore.addErrorListener(this.onError);
NotificationActions.getNotifications(this.props.url);
}
onChange(actionType) {
switch (actionType) {
case ACTIONS.RECEIVED_NOTIFICATIONS: {
this.setState({
count: NotificationsStore.getNotifications().length,
isLoaded: true
});
break;
}
case ACTIONS.NOTIFICATIONS_DRAWER_TOGGLE: {
this.setState({
drawerOpen: NotificationsStore.getIsDrawerOpen()
});
break;
}
default:
break;
}
}
onClick() {
NotificationActions.toggleNotificationDrawer();
}
iconType() {
return this.state.count === 0 ? 'fa-bell-o' : 'fa-bell';
}
render() {
return (
<a className="nav-item-iconic drawer-pf-trigger-icon" onClick={this.onClick}>
<span className={'fa ' + this.iconType()} title={__('Notifications')}></span>
</a>
);
}
}
export default NotificationDrawerToggle;
webpack/assets/javascripts/react_app/components/notifications/NotificationDrawerToggle.test.js
jest.unmock('./NotificationDrawerToggle');
jest.unmock('../../stores/NotificationsStore');
import testHelpers from '../../common/testHelpers';
import React from 'react';
import { shallow } from 'enzyme';
import NotificationDrawerToggle from './NotificationDrawerToggle';
import NotificationsStore from '../../stores/NotificationsStore';
import NotificationActions from '../../actions/NotificationActions';
function setup() {
return shallow(<NotificationDrawerToggle />);
}
describe('NotificationDrawerToggle', () => {
beforeEach(() => {
global.__ = (text) => text;
global.sessionStorage = testHelpers.mockStorage();
});
it('stores show/hide status in store', () => {
NotificationActions.getNotifications = jest.fn();
const wrapper = setup();
expect(NotificationsStore.getIsDrawerOpen()).toBe(false);
wrapper.simulate('click');
expect(NotificationsStore.getIsDrawerOpen()).toBe(true);
wrapper.simulate('click');
expect(NotificationsStore.getIsDrawerOpen()).toBe(false);
});
});
webpack/assets/javascripts/react_app/components/notifications/NotificationPanel.js
import React from 'react';
import NotificationPanelHeading from './NotificationPanelHeading';
import NotificationPanelBody from './NotificationPanelBody';
const NotificationPanel = ({notifications, title, id, expandedGroup}) => {
let unread;
if (notifications) {
unread = notifications.reduce((total, curr) => {
if (!curr.seen) {
total = total + 1;
}
return total;
}, 0);
} else {
unread = 0;
}
return (
<div className="panel panel-default">
<NotificationPanelHeading title={title} group={id} unread={unread}/>
<NotificationPanelBody notifications={notifications}
group={id} expandedGroup={expandedGroup}/>
</div>
);
};
export default NotificationPanel;
webpack/assets/javascripts/react_app/components/notifications/NotificationPanel.test.js
jest.unmock('./NotificationPanel');
import React from 'react';
import { shallow } from 'enzyme';
import NotificationPanel from './NotificationPanel';
function setup() {
return shallow(<NotificationPanel />);
}
describe('NotificationPanel', () => {
it('runs a test', () => {
setup();
expect('not implemented').toBeTruthy();
});
});
webpack/assets/javascripts/react_app/components/notifications/NotificationPanelBody.js
import React from 'react';
import Notification from './Notification';
import './Notifications.css';
const NotificationPanelBody = ({notifications, expandedGroup, group}) => {
let data;
let css = expandedGroup === group ? 'panel-body notification-panel-scroll' : 'hide';
if (notifications && notifications.length) {
data = notifications
.map(notification => <Notification key={notification.id} {...notification}></Notification>
);
} else {
data = null;
}
return (
<div className={css}>
{data}
</div>
);
};
export default NotificationPanelBody;
webpack/assets/javascripts/react_app/components/notifications/NotificationPanelBody.test.js
jest.unmock('./NotificationPanelBody');
import React from 'react';
import { shallow } from 'enzyme';
import NotificationPanelBody from './NotificationPanelBody';
function setup() {
return shallow(<NotificationPanelBody />);
}
describe('NotificationPanelBody', () => {
it('runs a test', () => {
setup();
expect('not implemented').toBeTruthy();
});
});
webpack/assets/javascripts/react_app/components/notifications/NotificationPanelHeading.js
import React from 'react';
import NotificationActions from '../../actions/NotificationActions';
const NotificationPanelHeading = ({group, unread, title}) => {
const styles = {textTransform: 'capitalize'};
function expandDrawerTab() {
NotificationActions.expandDrawerTab(group);
}
return (
<div className="panel-heading" onClick={expandDrawerTab}>
<h4 className="panel-title">
<a style={styles}>
{title}
</a>
</h4>
<span className="panel-counter">
{unread} New {unread !== 1 ? 'Events' : 'Event'}
</span>
</div>
);
};
export default NotificationPanelHeading;
webpack/assets/javascripts/react_app/components/notifications/NotificationPanelHeading.test.js
jest.unmock('./NotificationPanelHeading');
import React from 'react';
import { mount } from 'enzyme';
import NotificationPanelHeading from './NotificationPanelHeading';
function setup() {
return mount(<NotificationPanelHeading title="Panel Heading" unread="12" />);
}
describe('NotificationPanelHeading', () => {
it('shows title', () => {
const wrapper = setup();
expect(wrapper.find('.panel-title a').text()).toBe('Panel Heading');
});
it('shows number of unread', () => {
const wrapper = setup();
const counterText = wrapper.find('.panel-counter').text();
expect(counterText.substring(0, 2)).toBe('12');
});
});
webpack/assets/javascripts/react_app/components/notifications/Notifications.css
.notification-panel-scroll {
max-height: 364px;
overflow-y: auto;
}
webpack/assets/javascripts/react_app/constants.js
RECEIVED_STATISTICS: 'RECEIVED_STATISTICS',
STATISTICS_REQUEST_ERROR: 'STATISTICS_REQUEST_ERROR',
RECEIVED_HOSTS_POWER_STATE: 'RECEIVED_HOSTS_POWER_STATE',
HOSTS_REQUEST_ERROR: 'HOSTS_REQUEST_ERROR'
HOSTS_REQUEST_ERROR: 'HOSTS_REQUEST_ERROR',
RECEIVED_NOTIFICATIONS: 'RECEIVED_NOTIFICATIONS',
NOTIFICATIONS_REQUEST_ERROR: 'NOTIFICATIONS_REQUEST_ERROR',
NOTIFICATIONS_DRAWER_TOGGLE: 'NOTIFICATIONS_DRAWER_TOGGLE',
NOTIFICATIONS_EXPAND_DRAWER_TAB: 'NOTIFICATIONS_EXPAND_DRAWER_TAB',
NOTIFICATIONS_MARK_AS_READ: 'NOTIFICATIONS_MARK_AS_READ',
NOTIFICATIONS_MARKED_AS_READ: 'NOTIFICATIONS_MARKED_AS_READ'
};
export const STATUS = {
......
RESOLVED: 'RESOLVED',
ERROR: 'ERROR'
};
export const ICON_CSS = {
ok: 'pficon pficon-ok',
success: 'pficon pficon-ok',
info: 'pficon pficon-info',
warning: 'pficon pficon-warning-triangle-o',
error: 'pficon pficon-error-circle-o'
};
webpack/assets/javascripts/react_app/stores/NotificationsStore.js
import AppDispatcher from '../dispatcher';
import { ACTIONS } from '../constants';
import AppEventEmitter from './AppEventEmitter';
import moment from 'moment';
let _notifications = {};
let _expandedTab = null;
class NotificationsEventEmitter extends AppEventEmitter {
constructor() {
super();
}
getNotifications() {
return (_notifications.data || []);
}
getIsDrawerOpen() {
const value = window.sessionStorage.getItem('isDrawerOpen') || 'false';
return JSON.parse(value);
}
getExpandedGroup() {
return _expandedTab;
}
prepareNotifications(notifications) {
let preparedData = {};
let sortedData = {};
let keys;
notifications.forEach((notification) => {
const group = notification.group;
const value = notification;
if (!preparedData[group]) {
preparedData[group] = [value];
} else {
preparedData[group].push(value);
}
});
keys = Object.keys(preparedData);
keys.forEach((key) => {
sortedData[key] = preparedData[key].sort(compare);
});
return sortedData;
}
}
const NotificationsStore = new NotificationsEventEmitter();
/* eslint-disable max-statements */
AppDispatcher.register(action => {
switch (action.actionType) {
case ACTIONS.RECEIVED_NOTIFICATIONS: {
_notifications.data = NotificationsStore.prepareNotifications(action.notifications);
NotificationsStore.emitChange(action.actionType);
break;
}
case ACTIONS.NOTIFICATIONS_REQUEST_ERROR: {
NotificationsStore.emitError(action.info);
break;
}
case ACTIONS.NOTIFICATIONS_DRAWER_TOGGLE: {
const value = NotificationsStore.getIsDrawerOpen();
window.sessionStorage.setItem('isDrawerOpen', JSON.stringify(!value));
NotificationsStore.emitChange(action.actionType);
break;
}
case ACTIONS.NOTIFICATIONS_EXPAND_DRAWER_TAB: {
if (_expandedTab === action.expand) {
_expandedTab = null;
} else {
_expandedTab = action.expand;
}
NotificationsStore.emitChange(action.actionType);
break;
}
default:
// no op
break;
}
});
/* eslint-enable max-statements */
export default NotificationsStore;
// sort notifications by time descending
function compare(a, b) {
const diff = moment(a.created_at) - moment(b.created_at);
let returnValue;
if (diff < 0) {
returnValue = 1;
} else if (diff > 0) {
returnValue = -1;
} else {
returnValue = 0;
}
return returnValue;
}
webpack/assets/javascripts/react_app/stores/NotificationsStore.test.js
jest.unmock('./NotificationsStore');
jest.unmock('../constants');
jest.unmock('../dispatcher');
jest.unmock('../actions/ServerActions');
import NotificationsStore from './NotificationsStore';
import data from './NotificationsTestData';
describe('NotificationsStore', () => {
it('builds data structure', () => {
const result = NotificationsStore.prepareNotifications(data);
expect(Object.keys(result).length).toBe(4);
expect(result.test.length).toBe(2);
expect(result.info.length).toBe(2);
expect(result.error.length).toBe(1);
expect(result.warning.length).toBe(1);
});
it('sorts groups by date descending', () => {
const result = NotificationsStore.prepareNotifications(data);
expect(result.test[0].id).toBe(6);
expect(result.test[1].id).toBe(3);
expect(result.info[0].id).toBe(18);
expect(result.info[1].id).toBe(9);
});
});
webpack/assets/javascripts/react_app/stores/NotificationsTestData.js
const NotificationsData = [
{
'id': 3,
'level': 'success',
'created_at': '2016-12-13T16:52:47.187Z',
'text': 'test me',
'group': 'test',
'seen': false
},
{
'id': 6,
'level': 'success',
'created_at': '2016-12-13T16:54:23.024Z',
'text': 'test me',
'group': 'test',
'seen': false
},
{
'id': 9,
'level': 'info',
'created_at': '2016-12-26T11:47:13.912Z',
'text': 'FYI',
'group': 'info',
'seen': false
},
{
'id': 12,
'level': 'error',
'created_at': '2016-12-26T11:48:59.433Z',
'text': 'No no no',
'group': 'error',
'seen': false
},
{
'id': 15,
'level': 'warning',
'created_at': '2016-12-26T11:49:08.164Z',
'text': 'Be careful',
'group': 'warning',
'seen': false
},
{
'id': 18,
'level': 'info',
'created_at': '2016-12-26T11:49:17.520Z',
'text': 'FYI',
'group': 'info',
'seen': false
}
];
export default NotificationsData;

Also available in: Unified diff