diff --git a/src/components/navigation/www/navigation.jsx b/src/components/navigation/www/navigation.jsx index 515b29b82..9ed2e3cb4 100644 --- a/src/components/navigation/www/navigation.jsx +++ b/src/components/navigation/www/navigation.jsx @@ -27,7 +27,8 @@ class Navigation extends React.Component { super(props); bindAll(this, [ 'getProfileUrl', - 'handleSearchSubmit' + 'handleSearchSubmit', + 'setupMessagePolling' ]); this.state = { messageCountIntervalId: -1 // javascript method interval id for getting messsage count. @@ -35,27 +36,18 @@ class Navigation extends React.Component { } componentDidMount () { if (this.props.user) { - const intervalId = setInterval(() => { - this.props.getMessageCount(this.props.user.username); - }, 120000); // check for new messages every 2 mins. - this.setState({ // eslint-disable-line react/no-did-mount-set-state - messageCountIntervalId: intervalId - }); + // Setup polling for messages to start in 2 minutes. + setTimeout(this.setupMessagePolling.bind(this, 2), 2 * 60 * 1000); } } componentDidUpdate (prevProps) { if (prevProps.user !== this.props.user) { this.props.handleCloseAccountNav(); if (this.props.user) { - const intervalId = setInterval(() => { - this.props.getMessageCount(this.props.user.username); - }, 120000); // check for new messages every 2 mins. - this.setState({ // eslint-disable-line react/no-did-update-set-state - messageCountIntervalId: intervalId - }); + setTimeout(this.setupMessagePolling.bind(this, 2), 2 * 60 * 1000); } else { // clear message count check, and set to default id. - clearInterval(this.state.messageCountIntervalId); + clearTimeout(this.state.messageCountIntervalId); this.props.setMessageCount(0); this.setState({ // eslint-disable-line react/no-did-update-set-state messageCountIntervalId: -1 @@ -66,7 +58,7 @@ class Navigation extends React.Component { componentWillUnmount () { // clear message interval if it exists if (this.state.messageCountIntervalId !== -1) { - clearInterval(this.state.messageCountIntervalId); + clearTimeout(this.state.messageCountIntervalId); this.props.setMessageCount(0); this.setState({ messageCountIntervalId: -1 @@ -77,6 +69,23 @@ class Navigation extends React.Component { if (!this.props.user) return; return `/users/${this.props.user.username}/`; } + + setupMessagePolling (minutes) { + this.props.getMessageCount(this.props.user.username); + // We only poll if it has been less than 30 minutes. + // Chances of someone actively using the page for that long without + // a navigation is low. + if (minutes < 32) { + const nextFetch = minutes * 2; + const timeoutId = setTimeout(() => { + this.setupMessagePolling(nextFetch); + }, nextFetch * 60000); + this.setState({ // eslint-disable-line react/no-did-mount-set-state + messageCountIntervalId: timeoutId + }); + } + } + handleSearchSubmit (formData) { let targetUrl = '/search/projects'; if (formData.q) { diff --git a/test/unit/components/navigation.test.jsx b/test/unit/components/navigation.test.jsx index 9a9547a9a..e74bf125b 100644 --- a/test/unit/components/navigation.test.jsx +++ b/test/unit/components/navigation.test.jsx @@ -1,5 +1,5 @@ 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'; const Navigation = require('../../../src/components/navigation/www/navigation.jsx'); const Registration = require('../../../src/components/registration/registration.jsx'); @@ -11,6 +11,7 @@ describe('Navigation', () => { beforeEach(() => { store = null; + jest.useFakeTimers(); }); const getNavigationWrapper = props => { @@ -103,4 +104,88 @@ describe('Navigation', () => { navWrapper.find('a.registrationLink').simulate('click', {preventDefault () {}}); 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( + , {context: {store}, + childContextTypes: {store} + }); + + intlWrapper.children().find('Navigation') + .instance(); + const twoMin = 2 * 60 * 1000; + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin); + // Advance timers passed the intial two minutes. + jest.advanceTimersByTime(twoMin + 1); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin * 2); + expect(props.getMessageCount).toHaveBeenCalled(); + }); + + test('SetupMessagePolling 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( + , {context: {store}, + childContextTypes: {store} + }); + + const navInstance = intlWrapper.children().find('Navigation') + .instance(); + // Clear the timers and mocks because componentDidMount + // has already called setupMessagePolling. + jest.clearAllTimers(); + jest.clearAllMocks(); + + navInstance.setupMessagePolling(2); + + // Check that we set the timeout to backoff exponentially + let minutes = 2 * 60 * 1000; + for (let count = 1; count < 5; ++count) { + jest.advanceTimersByTime(minutes + 1); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), minutes * 2); + expect(props.getMessageCount).toHaveBeenCalledTimes(count); + minutes = minutes * 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 + // setupMessagePolling isn't counted here. + expect(setTimeout).toHaveBeenCalledTimes(4); + }); });