Unverified Commit ae014228 authored by picklesrus's avatar picklesrus Committed by GitHub

Merge pull request #4294 from LLK/hotfix/messages

[master] Hotfix/messages
parents a2651b74 05075712
...@@ -27,56 +27,62 @@ class Navigation extends React.Component { ...@@ -27,56 +27,62 @@ class Navigation extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'getProfileUrl', 'getProfileUrl',
'handleSearchSubmit' 'handleSearchSubmit',
'pollForMessages'
]); ]);
this.state = { // Keep the timeout id so we can cancel it (e.g. when we unmount)
messageCountIntervalId: -1 // javascript method interval id for getting messsage count. this.messageCountTimeoutId = -1;
};
} }
componentDidMount () { componentDidMount () {
if (this.props.user) { if (this.props.user) {
const intervalId = setInterval(() => { // Setup polling for messages to start in 2 minutes.
this.props.getMessageCount(this.props.user.username); const twoMinInMs = 2 * 60 * 1000;
}, 120000); // check for new messages every 2 mins. this.messageCountTimeoutId = setTimeout(this.pollForMessages.bind(this, twoMinInMs), twoMinInMs);
this.setState({ // eslint-disable-line react/no-did-mount-set-state
messageCountIntervalId: intervalId
});
} }
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.user !== this.props.user) { if (prevProps.user !== this.props.user) {
this.props.handleCloseAccountNav(); this.props.handleCloseAccountNav();
if (this.props.user) { if (this.props.user) {
const intervalId = setInterval(() => { const twoMinInMs = 2 * 60 * 1000;
this.props.getMessageCount(this.props.user.username); this.messageCountTimeoutId = setTimeout(this.pollForMessages.bind(this, twoMinInMs), twoMinInMs);
}, 120000); // check for new messages every 2 mins.
this.setState({ // eslint-disable-line react/no-did-update-set-state
messageCountIntervalId: intervalId
});
} else { } else {
// clear message count check, and set to default id. // Clear message count check, and set to default id.
clearInterval(this.state.messageCountIntervalId); if (this.messageCountTimeoutId !== -1) {
clearTimeout(this.messageCountTimeoutId);
}
this.props.setMessageCount(0); this.props.setMessageCount(0);
this.setState({ // eslint-disable-line react/no-did-update-set-state this.messageCountTimeoutId = -1;
messageCountIntervalId: -1
});
} }
} }
} }
componentWillUnmount () { componentWillUnmount () {
// clear message interval if it exists // clear message interval if it exists
if (this.state.messageCountIntervalId !== -1) { if (this.messageCountTimeoutId !== -1) {
clearInterval(this.state.messageCountIntervalId); clearTimeout(this.messageCountTimeoutId);
this.props.setMessageCount(0); this.props.setMessageCount(0);
this.setState({ this.messageCountTimeoutId = -1;
messageCountIntervalId: -1
});
} }
} }
getProfileUrl () { getProfileUrl () {
if (!this.props.user) return; if (!this.props.user) return;
return `/users/${this.props.user.username}/`; return `/users/${this.props.user.username}/`;
} }
pollForMessages (ms) {
this.props.getMessageCount(this.props.user.username);
// We only poll if it has been less than 32 minutes.
// Chances of someone actively using the page for that long without
// a navigation is low.
if (ms < 32 * 60 * 1000) { // 32 minutes
const nextFetch = ms * 2; // exponentially back off next fetch time.
const timeoutId = setTimeout(() => {
this.pollForMessages(nextFetch);
}, nextFetch);
this.messageCountTimeoutId = timeoutId;
}
}
handleSearchSubmit (formData) { handleSearchSubmit (formData) {
let targetUrl = '/search/projects'; let targetUrl = '/search/projects';
if (formData.q) { if (formData.q) {
......
const React = require('react'); const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx'); const {shallowWithIntl, mountWithIntl} = require('../../helpers/intl-helpers.jsx');
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
const Navigation = require('../../../src/components/navigation/www/navigation.jsx'); const Navigation = require('../../../src/components/navigation/www/navigation.jsx');
const Registration = require('../../../src/components/registration/registration.jsx'); const Registration = require('../../../src/components/registration/registration.jsx');
...@@ -11,6 +11,7 @@ describe('Navigation', () => { ...@@ -11,6 +11,7 @@ describe('Navigation', () => {
beforeEach(() => { beforeEach(() => {
store = null; store = null;
jest.useFakeTimers();
}); });
const getNavigationWrapper = props => { const getNavigationWrapper = props => {
...@@ -103,4 +104,123 @@ describe('Navigation', () => { ...@@ -103,4 +104,123 @@ describe('Navigation', () => {
navWrapper.find('a.registrationLink').simulate('click', {preventDefault () {}}); navWrapper.find('a.registrationLink').simulate('click', {preventDefault () {}});
expect(navInstance.props.handleClickRegistration).toHaveBeenCalled(); expect(navInstance.props.handleClickRegistration).toHaveBeenCalled();
}); });
test('Component sets up message polling when it mounts', () => {
store = mockStore({
navigation: {
registrationOpen: false
},
messageCount: {
messageCount: 5
}
});
const props = {
user: {
thumbnailUrl: 'scratch.mit.edu',
username: 'auser'
},
getMessageCount: jest.fn()
};
const intlWrapper = mountWithIntl(
<Navigation
{...props}
/>, {context: {store},
childContextTypes: {store}
});
const navInstance = intlWrapper.children().find('Navigation')
.instance();
const twoMin = 2 * 60 * 1000;
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin);
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
// Advance timers passed the intial two minutes.
jest.advanceTimersByTime(twoMin + 1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin * 2);
expect(props.getMessageCount).toHaveBeenCalled();
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
});
test('Component cancels timers when it unmounts', () => {
store = mockStore({
navigation: {
registrationOpen: false
},
messageCount: {
messageCount: 5
}
});
const props = {
user: {
thumbnailUrl: 'scratch.mit.edu',
username: 'auser'
},
getMessageCount: jest.fn()
};
const intlWrapper = mountWithIntl(
<Navigation
{...props}
/>, {context: {store},
childContextTypes: {store}
});
const navInstance = intlWrapper.children().find('Navigation')
.instance();
const twoMin = 2 * 60 * 1000;
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin);
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
navInstance.componentWillUnmount();
expect(clearTimeout).toHaveBeenCalledWith(expect.any(Number));
expect(navInstance.messageCountTimeoutId).toEqual(-1);
});
test('pollForMessages polls for messages 5 times', () => {
store = mockStore({
navigation: {
registrationOpen: false
},
messageCount: {
messageCount: 5
}
});
const props = {
user: {
thumbnailUrl: 'scratch.mit.edu',
username: 'auser'
},
getMessageCount: jest.fn()
};
const intlWrapper = mountWithIntl(
<Navigation
{...props}
/>, {context: {store},
childContextTypes: {store}
});
const navInstance = intlWrapper.children().find('Navigation')
.instance();
// Clear the timers and mocks because componentDidMount
// has already called pollForMessages.
jest.clearAllTimers();
jest.clearAllMocks();
let twoMinInMs = 2 * 60 * 1000; // 2 minutes in ms.
navInstance.pollForMessages(twoMinInMs);
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
// Check that we set the timeout to backoff exponentially
for (let count = 1; count < 5; ++count) {
jest.advanceTimersByTime(twoMinInMs + 1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMinInMs * 2);
expect(props.getMessageCount).toHaveBeenCalledTimes(count);
twoMinInMs = twoMinInMs * 2;
}
// Exhaust all timers (there shouldn't be any left)
jest.runAllTimers();
// We exponentially back off checking for messages, starting at 2 min
// and stop after 32 minutes so it should happen 5 times total.
expect(props.getMessageCount).toHaveBeenCalledTimes(5);
// setTimeout happens 1 fewer since the original call to
// pollForMessages isn't counted here.
expect(setTimeout).toHaveBeenCalledTimes(4);
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment