From 377806fd1f0cc20903702cbf2671d0d8f9ab6480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Wi=C5=9Bniewski?= Date: Thu, 20 Aug 2020 09:22:41 +0200 Subject: [PATCH 1/2] timeago component refactor --- config/jest.config.js | 2 + package.json | 1 + .../__snapshots__/Card.spec.js.snap | 25 +++ .../Timeago/__snapshots__/index.spec.js.snap | 38 ++++- source/components/Timeago/index.js | 121 +++++++------- source/components/Timeago/index.spec.js | 153 +++++++++++++++++- source/utils/timezone.spec.js | 5 + yarn.lock | 5 + 8 files changed, 287 insertions(+), 63 deletions(-) create mode 100644 source/utils/timezone.spec.js diff --git a/config/jest.config.js b/config/jest.config.js index ebba9100..84f4d8b9 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -1,3 +1,5 @@ +process.env.TZ = 'UTC'; + module.exports = { rootDir: '..', moduleNameMapper: { diff --git a/package.json b/package.json index 0d3083dc..de97cf2c 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "i18next": "^19.3.2", "immutable": "^4.0.0-rc.12", "lodash": "^4.17.15", + "mockdate": "^3.0.2", "prop-types": "^15.7.2", "react": "^16.13.0", "react-dom": "^16.13.0", diff --git a/source/components/GlobalNavigation/components/Notifications/__snapshots__/Card.spec.js.snap b/source/components/GlobalNavigation/components/Notifications/__snapshots__/Card.spec.js.snap index 0fa34941..74fa4e91 100644 --- a/source/components/GlobalNavigation/components/Notifications/__snapshots__/Card.spec.js.snap +++ b/source/components/GlobalNavigation/components/Notifications/__snapshots__/Card.spec.js.snap @@ -156,6 +156,11 @@ exports[`Card renders correctly with default props 1`] = ` >
  • @@ -332,6 +337,11 @@ exports[`Card renders correctly with isUnread set to true 1`] = ` >
  • @@ -513,6 +523,11 @@ exports[`Card renders correctly with no title 1`] = ` >
  • @@ -672,6 +687,11 @@ exports[`Card renders correctly with no title and announcement type 1`] = ` >
  • @@ -848,6 +868,11 @@ exports[`Card renders correctly with two actors 1`] = ` >
  • diff --git a/source/components/Timeago/__snapshots__/index.spec.js.snap b/source/components/Timeago/__snapshots__/index.spec.js.snap index a0630b40..cede660d 100644 --- a/source/components/Timeago/__snapshots__/index.spec.js.snap +++ b/source/components/Timeago/__snapshots__/index.spec.js.snap @@ -1,9 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Renders Timeago component 1`] = ` - - 2/20/2020 - + customMessage + +`; + +exports[`Timeago now should render custom string 2`] = ` + +`; + +exports[`Timeago now should render default string 1`] = ` + +`; + +exports[`Timeago now should render default string 2`] = ` + `; diff --git a/source/components/Timeago/index.js b/source/components/Timeago/index.js index 533150d8..cb451d57 100644 --- a/source/components/Timeago/index.js +++ b/source/components/Timeago/index.js @@ -1,70 +1,81 @@ import React from 'react'; import PropTypes from 'prop-types'; -/* istanbul ignore next */ -function getTimeDistanceString(datetime) { - const date = new Date(parseInt(datetime, 10)); - const now = Date.now(); - const diffInSeconds = (now - date) / 1000; +export const SECOND_MILLISECONDS = 1000; +export const MINUTE_SECONDS = 60; +export const HOUR_SECONDS = 60 * MINUTE_SECONDS; +export const DAY_SECONDS = 24 * HOUR_SECONDS; +export const FIVE_DAYS_SECONDS = 5 * DAY_SECONDS; - if (diffInSeconds > 432000) { - // more than 5 days ago - show date - return date.toLocaleDateString(); - } - - if (diffInSeconds > 86400) { - // more than a day ago - return `${Math.round(diffInSeconds / 60 / 60 / 24)}d`; - } - - if (diffInSeconds > 3600) { - // more than an hour ago - return `${Math.round(diffInSeconds / 60 / 60)}h`; - } - - if (diffInSeconds < 60) { - // less than a minute ago - return 'now'; - } - - return `${Math.round(diffInSeconds / 60)}m`; -} +export const TIMEAGO_SHOW_DATE_THRESHOLD = FIVE_DAYS_SECONDS; +export const TIMEAGO_NOW_THRESHOLD = MINUTE_SECONDS; /** * The Timeago component is a small component that * shows the number of seconds/minutes/days from given datetime. - * - * It all happens after the component is mounted so it's safe to use this - * component on the Back-End without messing up the hydration. */ -export default class Timeago extends React.Component { - state = { - display: this.props.datetime, - }; +export default function Timeago({ + datetime, renderNow, renderDate, renderDays, renderHours, renderMinutes, +}) { + const date = new Date(datetime); - static propTypes = { - datetime: PropTypes.oneOfType([ - PropTypes.instanceOf(Date), - PropTypes.string, - ]).isRequired, - }; + const withCustomRender = (renderFunction, value, defaultElement = value) => (typeof (renderFunction) === 'function' ? renderFunction(value) : defaultElement); + + const renderTime = () => { + const now = Date.now(); + const diffInSeconds = (now - date.getTime()) / SECOND_MILLISECONDS; - componentDidMount() { - const { datetime } = this.props; + if (diffInSeconds > TIMEAGO_SHOW_DATE_THRESHOLD) { + const dateString = date.toLocaleDateString(); - this.setState({ - display: getTimeDistanceString(datetime), - }); - } + return withCustomRender(renderDate, dateString); + } - render() { - const { datetime } = this.props; - const { display } = this.state; + if (diffInSeconds > DAY_SECONDS) { + // more than a day ago + const days = Math.round(diffInSeconds / DAY_SECONDS); + return withCustomRender(renderDays, days, `${days}d`); + } - return ( - - {display} - - ); - } + if (diffInSeconds > HOUR_SECONDS) { + // more than an hour ago + const hours = Math.round(diffInSeconds / HOUR_SECONDS); + return withCustomRender(renderHours, hours, `${hours}h`); + } + + if (diffInSeconds > TIMEAGO_NOW_THRESHOLD) { + const minutes = Math.round(diffInSeconds / MINUTE_SECONDS); + return withCustomRender(renderMinutes, minutes, `${minutes}m`); + } + + // less than a minute ago + return typeof (renderNow) === 'function' ? renderNow() : 'now'; + }; + + + return ( + + ); } + +Timeago.propTypes = { + datetime: PropTypes.oneOfType([ + PropTypes.instanceOf(Date), + PropTypes.string, + ]).isRequired, + renderDate: PropTypes.func, + renderDays: PropTypes.func, + renderHours: PropTypes.func, + renderMinutes: PropTypes.func, + renderNow: PropTypes.func, +}; + +Timeago.defaultProps = { + renderDays: null, + renderHours: null, + renderMinutes: null, + renderDate: null, + renderNow: null, +}; diff --git a/source/components/Timeago/index.spec.js b/source/components/Timeago/index.spec.js index b40e6a97..1bbbc9a3 100644 --- a/source/components/Timeago/index.spec.js +++ b/source/components/Timeago/index.spec.js @@ -1,10 +1,157 @@ import React from 'react'; import { shallow } from 'enzyme'; +import MockDate from 'mockdate'; import Timeago from './index'; -test('Renders Timeago component', () => { - const component = shallow(); +const getNowDate = () => new Date('Tue Sep 17 2018 12:58:43 GMT+0000'); - expect(component).toMatchSnapshot(); +beforeAll(() => { + MockDate.set(getNowDate()); +}); + +afterAll(() => { + MockDate.reset(); +}); + +describe('Timeago', () => { + describe('now', () => { + const date = getNowDate(); + const dataSet = [ + date, + `${date}`, + ]; + + test('should render default string', () => { + dataSet.forEach(datetime => { + const component = shallow(); + + expect(component.text()).toEqual('now'); + expect(component).toMatchSnapshot(); + }); + }); + + dataSet.forEach(datetime => { + test('should render custom string', () => { + const customMessage = 'customMessage'; + const renderNow = () => customMessage; + const component = shallow(); + + expect(component.text()).toEqual(customMessage); + expect(component).toMatchSnapshot(); + }); + }); + }); + + describe('minutes ago', () => { + const date = getNowDate(); + date.setMinutes(date.getMinutes() - 5); + + const dataSet = [ + date, + `${date}`, + ]; + + dataSet.forEach(datetime => { + test('should render default string', () => { + const component = shallow(); + + expect(component.text()).toEqual('5m'); + }); + }); + + dataSet.forEach(datetime => { + test('should render custom string', () => { + const renderMinutes = (minutes) => `${minutes} minutes ago`; + + const component = shallow(); + + expect(component.text()).toEqual('5 minutes ago'); + }); + }); + }); + + describe('hours ago', () => { + const date = getNowDate(); + date.setHours(date.getHours() - 8); + + const dataSet = [ + date, + `${date}`, + ]; + + dataSet.forEach(datetime => { + test('should render default string', () => { + const component = shallow(); + + expect(component.text()).toEqual('8h'); + }); + }); + + dataSet.forEach(datetime => { + test('should render custom string', () => { + const renderHours = (hours) => `${hours} hours ago`; + + const component = shallow(); + + expect(component.text()).toEqual('8 hours ago'); + }); + }); + }); + + describe('days ago', () => { + const date = getNowDate(); + date.setDate(date.getDate() - 2); + + const dataSet = [ + date, + `${date}`, + ]; + + dataSet.forEach(datetime => { + test('should render default string', () => { + const component = shallow(); + + expect(component.text()).toEqual('2d'); + }); + }); + + dataSet.forEach(datetime => { + test('should render custom string', () => { + const renderDays = (days) => `${days} days ago`; + + const component = shallow(); + + expect(component.text()).toEqual('2 days ago'); + }); + }); + }); + + describe('date', () => { + const date = getNowDate(); + date.setMonth(date.getMonth() - 3); + + const dataSet = [ + date, + `${date}`, + ]; + + dataSet.forEach(datetime => { + test('should render default string', () => { + const component = shallow(); + + expect(component.text()).toEqual(date.toLocaleDateString()); + }); + }); + + dataSet.forEach(datetime => { + test('should render custom string', () => { + const renderDate = (dateString) => `created on ${dateString}`; + + const component = shallow(); + + expect(component.text()).toEqual(`created on ${date.toLocaleDateString()}`); + }); + }); + }); }); diff --git a/source/utils/timezone.spec.js b/source/utils/timezone.spec.js new file mode 100644 index 00000000..9692b909 --- /dev/null +++ b/source/utils/timezone.spec.js @@ -0,0 +1,5 @@ +describe('Timezone', () => { + test('should be UTC', () => { + expect(new Date().getTimezoneOffset()).toBe(0); + }); +}); diff --git a/yarn.lock b/yarn.lock index d4c0c536..935d9f4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10058,6 +10058,11 @@ mktemp@~0.4.0: resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" integrity sha1-bQUVYRyKjITkhKogABKbmOmB/ws= +mockdate@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.2.tgz#a5a7bb5820da617747af424d7a4dcb22c6c03d79" + integrity sha512-ldfYSUW1ocqSHGTK6rrODUiqAFPGAg0xaHqYJ5tvj1hQyFsjuHpuWgWFTZWwDVlzougN/s2/mhDr8r5nY5xDpA== + moo@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" From 395cba956ffeb9dea05f8ca743c23ae73e5e812a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Wi=C5=9Bniewski?= Date: Thu, 20 Aug 2020 09:50:10 +0200 Subject: [PATCH 2/2] fix tests --- .../Timeago/__snapshots__/index.spec.js.snap | 16 ++++++++-------- source/components/Timeago/index.spec.js | 10 +++++++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/source/components/Timeago/__snapshots__/index.spec.js.snap b/source/components/Timeago/__snapshots__/index.spec.js.snap index cede660d..9f22efe2 100644 --- a/source/components/Timeago/__snapshots__/index.spec.js.snap +++ b/source/components/Timeago/__snapshots__/index.spec.js.snap @@ -2,8 +2,8 @@ exports[`Timeago now should render custom string 1`] = ` @@ -11,8 +11,8 @@ exports[`Timeago now should render custom string 1`] = ` exports[`Timeago now should render custom string 2`] = ` @@ -20,8 +20,8 @@ exports[`Timeago now should render custom string 2`] = ` exports[`Timeago now should render default string 1`] = ` @@ -29,8 +29,8 @@ exports[`Timeago now should render default string 1`] = ` exports[`Timeago now should render default string 2`] = ` diff --git a/source/components/Timeago/index.spec.js b/source/components/Timeago/index.spec.js index 1bbbc9a3..fe2862f8 100644 --- a/source/components/Timeago/index.spec.js +++ b/source/components/Timeago/index.spec.js @@ -4,14 +4,22 @@ import MockDate from 'mockdate'; import Timeago from './index'; -const getNowDate = () => new Date('Tue Sep 17 2018 12:58:43 GMT+0000'); +const getNowDate = () => new Date('Tue Sep 17 2018 12:58:43'); + +const RealDate = Date; beforeAll(() => { MockDate.set(getNowDate()); + Date.prototype = Object.assign(Date.prototype, { + toLocaleString() { + return this.toISOString(); + }, + }); }); afterAll(() => { MockDate.reset(); + global.Date = RealDate; }); describe('Timeago', () => {