From 2855caf98e687f3c96cfd293d6cf82dc04548cd5 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Tue, 24 Mar 2020 18:41:14 -0400 Subject: [PATCH 01/18] add SaveMessage component to Header --- web/src/components/Header.js | 4 +- web/src/components/SaveMessage.js | 71 +++++++++++++++ web/src/components/SaveMessage.test.js | 115 +++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 web/src/components/SaveMessage.js create mode 100644 web/src/components/SaveMessage.test.js diff --git a/web/src/components/Header.js b/web/src/components/Header.js index 6f1a60450b..939292fa8e 100644 --- a/web/src/components/Header.js +++ b/web/src/components/Header.js @@ -8,6 +8,7 @@ import { getIsAdmin } from '../reducers/user.selector'; import { t } from '../i18n'; import DashboardButton from './DashboardButton'; +import SaveMessage from './SaveMessage'; import Icon, { Check, @@ -86,7 +87,8 @@ class Header extends Component { ) : ( - Saved {lastSaved} + + )} diff --git a/web/src/components/SaveMessage.js b/web/src/components/SaveMessage.js new file mode 100644 index 0000000000..01a8ce3742 --- /dev/null +++ b/web/src/components/SaveMessage.js @@ -0,0 +1,71 @@ +import React from "react"; +import moment from "moment"; + +// Configure moment to display '1 time-unit ago' instead of 'a time-unit ago' +// https://github.com/moment/moment/issues/3764 +moment.updateLocale("en", { + relativeTime: { + s: "seconds", + m: "1 minute", + mm: "%d minutes", + h: "1 hour", + hh: "%d hours", + d: "1 day", + dd: "%d days", + M: "1 month", + MM: "%d months", + y: "1 year", + yy: "%d years", + }, +}); + +class SaveMessage extends React.Component { + constructor(props) { + super(props); + + this.state = { + currentMoment: moment(), + }; + } + + componentDidMount() { + this.timerID = setInterval(() => this.updateClock(), 1000); + } + + componentWillUnmount() { + clearInterval(this.timerID); + } + + updateClock() { + this.setState({ + currentMoment: moment(), + }); + } + + render() { + const { lastSaved } = this.props; + const { currentMoment } = this.state; + + const lastSavedMoment = moment(lastSaved); + const minutesOld = currentMoment.diff(lastSavedMoment, "minutes"); + const minutesPerDay = 60 * 24; + const minutesPerYear = minutesPerDay * 365.25; + let result = "Last saved"; + + if (minutesOld < 1) return "Saved"; + if (1 <= minutesOld && minutesOld < minutesPerDay) { + // https://momentjs.com/docs/#/displaying/format/ + result = `${result} ${lastSavedMoment.format("hh:mm a")}`; + } + if (minutesPerDay <= minutesOld && minutesOld < minutesPerYear) { + result = `${result} ${lastSavedMoment.format("MMMM D")}`; + } + if (minutesPerYear <= minutesOld) { + result = `${result} ${lastSavedMoment.format("MMMM D, YYYY")}`; + } + result = `${result} (${lastSavedMoment.fromNow()})`; + return result; + } +} + +export default SaveMessage; diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js new file mode 100644 index 0000000000..7abb958825 --- /dev/null +++ b/web/src/components/SaveMessage.test.js @@ -0,0 +1,115 @@ +import React from "react"; +import { shallow } from "enzyme"; +import moment from "moment"; +import SaveMessage from "./SaveMessage"; + +describe("", () => { + let lastSaved, subject; + + describe('when saved less than 1 minute ago, it displays "Saved"', () => { + [ + ["1 second ago", 1], + ["2 seconds ago", 2], + ["30 seconds ago", 30], + ["59 seconds", 59], + ].forEach(([testName, seconds]) => { + test(testName, () => { + lastSaved = moment().subtract(seconds, "seconds"); + subject = shallow( + + ); + expect(subject.text()).toEqual("Saved"); + }); + }); + }); + + describe("given current time is January 1, 2020 12:00 pm", () => { + let jan1AtNoon = new Date(2020, 0, 1, 12, 0); + let mockDateNow; + + beforeEach(() => { + mockDateNow = jest + .spyOn(Date, "now") + .mockReturnValue(jan1AtNoon.getTime()); + }); + + afterEach(() => { + mockDateNow.mockRestore(); + }); + + describe("when saved 1 minute ago", () => { + it('displays "Last saved 11:59 am (1 minute ago)"', () => { + lastSaved = moment(jan1AtNoon).subtract(1, "minutes"); + subject = shallow( + + ); + expect(subject.text().startsWith("Last saved")).toEqual(true); + expect(subject.text().includes("11:59 am")).toEqual(true); + expect(subject.text().endsWith("(1 minute ago)")).toEqual(true); + }); + }); + + describe("when saved 23 hours and 59 minutes ago", () => { + it('displays "Last saved 12:01 pm (1 day ago)"', () => { + lastSaved = moment(jan1AtNoon) + .subtract(23, "hours") + .subtract(59, "minutes"); + subject = shallow( + + ); + expect(subject.text().startsWith("Last saved")).toEqual(true); + expect(subject.text().includes("12:01 pm")).toEqual(true); + expect(subject.text().endsWith("(1 day ago)")).toEqual(true); + }); + }); + + describe("when saved 24 hours ago", () => { + it('displays "Last saved December 30 (1 day ago)"', () => { + lastSaved = moment().subtract(1, "day"); + subject = shallow( + + ); + expect(subject.text().startsWith("Last saved")).toEqual(true); + expect(subject.text().includes("December 31")).toEqual(true); + expect(subject.text().endsWith("(1 day ago)")).toEqual(true); + }); + }); + + describe("when saved 30 days ago", () => { + it('displays "Last saved December 30 (1 month ago)"', () => { + lastSaved = moment().subtract(30, "day"); + subject = shallow( + + ); + expect(subject.text().startsWith("Last saved")).toEqual(true); + expect(subject.text().includes("December 2")).toEqual(true); + expect(subject.text().endsWith("(1 month ago)")).toEqual(true); + }); + }); + + describe("when saved 354 days ago", () => { + it('displays "Last saved January 2 (1 year ago)"', () => { + lastSaved = moment().subtract(364, "day"); + subject = shallow( + + ); + expect(subject.text().startsWith("Last saved")).toEqual(true); + expect(subject.text().includes("January 2")).toEqual(true); + expect(subject.text().includes("2019")).toEqual(false); + expect(subject.text().endsWith("(1 year ago)")).toEqual(true); + }); + }); + + describe("when saved 3 years ago", () => { + it('displays "Last saved January 1, 2017 (3 years ago)"', () => { + lastSaved = moment().subtract(3, "years"); + subject = shallow( + + ); + expect(subject.text().startsWith("Last saved")).toEqual(true); + expect(subject.text().includes("January 1, 2017")).toEqual(true); + expect(subject.text().endsWith("(3 years ago)")).toEqual(true); + }); + }); + }); +}); From f9071f0bc077af9674f26156b21fd47c1954ac63 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Wed, 25 Mar 2020 10:47:53 -0400 Subject: [PATCH 02/18] preserve space between Check and SaveMessage components --- web/src/components/Header.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/components/Header.js b/web/src/components/Header.js index 939292fa8e..3f8faebe58 100644 --- a/web/src/components/Header.js +++ b/web/src/components/Header.js @@ -87,8 +87,7 @@ class Header extends Component { ) : ( - - + )} From 53f3b9b721687c41d1ea59aa660c00622b290344 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Wed, 25 Mar 2020 12:13:38 -0400 Subject: [PATCH 03/18] use moment#duration to calculate time differences, use expect#toMatch for better errors --- web/src/components/SaveMessage.js | 15 +++++---- web/src/components/SaveMessage.test.js | 45 +++++++++++++------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/web/src/components/SaveMessage.js b/web/src/components/SaveMessage.js index 01a8ce3742..620d1c8470 100644 --- a/web/src/components/SaveMessage.js +++ b/web/src/components/SaveMessage.js @@ -47,23 +47,24 @@ class SaveMessage extends React.Component { const { currentMoment } = this.state; const lastSavedMoment = moment(lastSaved); - const minutesOld = currentMoment.diff(lastSavedMoment, "minutes"); - const minutesPerDay = 60 * 24; - const minutesPerYear = minutesPerDay * 365.25; + const difference = currentMoment.diff(lastSavedMoment); + const duration = moment.duration(difference); let result = "Last saved"; - if (minutesOld < 1) return "Saved"; - if (1 <= minutesOld && minutesOld < minutesPerDay) { + if (duration.asMinutes() < 1) return "Saved"; + + if (1 <= duration.asMinutes() && duration.asDays() < 1) { // https://momentjs.com/docs/#/displaying/format/ result = `${result} ${lastSavedMoment.format("hh:mm a")}`; } - if (minutesPerDay <= minutesOld && minutesOld < minutesPerYear) { + if (1 <= duration.asDays() && duration.asYears() < 1) { result = `${result} ${lastSavedMoment.format("MMMM D")}`; } - if (minutesPerYear <= minutesOld) { + if (1 <= duration.asYears()) { result = `${result} ${lastSavedMoment.format("MMMM D, YYYY")}`; } result = `${result} (${lastSavedMoment.fromNow()})`; + return result; } } diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index 7abb958825..633495c0bd 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -43,9 +43,9 @@ describe("", () => { subject = shallow( ); - expect(subject.text().startsWith("Last saved")).toEqual(true); - expect(subject.text().includes("11:59 am")).toEqual(true); - expect(subject.text().endsWith("(1 minute ago)")).toEqual(true); + expect(subject.text()).toMatch(/^Last saved/); + expect(subject.text()).toMatch(/11:59 am/); + expect(subject.text()).toMatch(/\(1 minute ago\)$/); }); }); @@ -57,46 +57,47 @@ describe("", () => { subject = shallow( ); - expect(subject.text().startsWith("Last saved")).toEqual(true); - expect(subject.text().includes("12:01 pm")).toEqual(true); - expect(subject.text().endsWith("(1 day ago)")).toEqual(true); + expect(subject.text()).toMatch(/^Last saved/); + expect(subject.text()).toMatch(/12:01 pm/); + expect(subject.text()).toMatch(/\(1 day ago\)$/); }); }); - describe("when saved 24 hours ago", () => { + describe("when saved 1 day ago", () => { it('displays "Last saved December 30 (1 day ago)"', () => { lastSaved = moment().subtract(1, "day"); subject = shallow( ); - expect(subject.text().startsWith("Last saved")).toEqual(true); - expect(subject.text().includes("December 31")).toEqual(true); - expect(subject.text().endsWith("(1 day ago)")).toEqual(true); + expect(subject.text()).toMatch(/^Last saved/); + expect(subject.text()).toMatch(/December 31/); + expect(subject.text()).toMatch(/\(1 day ago\)$/); }); }); describe("when saved 30 days ago", () => { - it('displays "Last saved December 30 (1 month ago)"', () => { + it('displays "Last saved December 2 (1 month ago)"', () => { lastSaved = moment().subtract(30, "day"); subject = shallow( ); - expect(subject.text().startsWith("Last saved")).toEqual(true); - expect(subject.text().includes("December 2")).toEqual(true); - expect(subject.text().endsWith("(1 month ago)")).toEqual(true); + expect(subject.text()).toMatch(/^Last saved/); + expect(subject.text()).toMatch(/December 2/); + expect(subject.text()).not.toMatch(/2019/); + expect(subject.text()).toMatch(/\(1 month ago\)$/); }); }); - describe("when saved 354 days ago", () => { + describe("when saved 364 days ago", () => { it('displays "Last saved January 2 (1 year ago)"', () => { lastSaved = moment().subtract(364, "day"); subject = shallow( ); - expect(subject.text().startsWith("Last saved")).toEqual(true); - expect(subject.text().includes("January 2")).toEqual(true); - expect(subject.text().includes("2019")).toEqual(false); - expect(subject.text().endsWith("(1 year ago)")).toEqual(true); + expect(subject.text()).toMatch(/^Last saved/); + expect(subject.text()).toMatch(/January 2/); + expect(subject.text()).not.toMatch(/2019/); + expect(subject.text()).toMatch(/\(1 year ago\)$/); }); }); @@ -106,9 +107,9 @@ describe("", () => { subject = shallow( ); - expect(subject.text().startsWith("Last saved")).toEqual(true); - expect(subject.text().includes("January 1, 2017")).toEqual(true); - expect(subject.text().endsWith("(3 years ago)")).toEqual(true); + expect(subject.text()).toMatch(/^Last saved/); + expect(subject.text()).toMatch(/January 1, 2017/); + expect(subject.text()).toMatch(/\(3 years ago\)$/); }); }); }); From 66aab17e43cd1924584845fb541ebbaef13a81ee Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Wed, 25 Mar 2020 17:55:42 -0400 Subject: [PATCH 04/18] appease the demands of our linter --- web/src/components/SaveMessage.js | 25 ++++++++++++++++++++----- web/src/components/SaveMessage.test.js | 5 +++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/web/src/components/SaveMessage.js b/web/src/components/SaveMessage.js index 620d1c8470..592cfca627 100644 --- a/web/src/components/SaveMessage.js +++ b/web/src/components/SaveMessage.js @@ -1,5 +1,6 @@ -import React from "react"; import moment from "moment"; +import React from "react"; +import PropTypes from 'prop-types'; // Configure moment to display '1 time-unit ago' instead of 'a time-unit ago' // https://github.com/moment/moment/issues/3764 @@ -53,14 +54,16 @@ class SaveMessage extends React.Component { if (duration.asMinutes() < 1) return "Saved"; - if (1 <= duration.asMinutes() && duration.asDays() < 1) { - // https://momentjs.com/docs/#/displaying/format/ + // eslint's "yoda": "Expected literal to be on the right side of <=" + // Which is easier to visualize on a number line, Mr. Yoda? + // lowerBound <= object.value() && object.value() < upperBound // or... + if (duration.asMinutes() <= 1 && duration.asDays() < 1) { result = `${result} ${lastSavedMoment.format("hh:mm a")}`; } - if (1 <= duration.asDays() && duration.asYears() < 1) { + if (duration.asDays() <= 1 && duration.asYears() < 1) { result = `${result} ${lastSavedMoment.format("MMMM D")}`; } - if (1 <= duration.asYears()) { + if (duration.asYears() <= 1) { result = `${result} ${lastSavedMoment.format("MMMM D, YYYY")}`; } result = `${result} (${lastSavedMoment.fromNow()})`; @@ -69,4 +72,16 @@ class SaveMessage extends React.Component { } } +SaveMessage.propTypes = { + lastSaved: PropTypes.oneOfType([ + PropTypes.instanceOf(Date), + PropTypes.instanceOf(moment), + PropTypes.string + ].isRequred), +}; + +SaveMessage.defaultProps = { + lastSaved: moment(), +}; + export default SaveMessage; diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index 633495c0bd..7f43f2d8fa 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -4,7 +4,8 @@ import moment from "moment"; import SaveMessage from "./SaveMessage"; describe("", () => { - let lastSaved, subject; + let lastSaved; + let subject; describe('when saved less than 1 minute ago, it displays "Saved"', () => { [ @@ -24,7 +25,7 @@ describe("", () => { }); describe("given current time is January 1, 2020 12:00 pm", () => { - let jan1AtNoon = new Date(2020, 0, 1, 12, 0); + const jan1AtNoon = new Date(2020, 0, 1, 12, 0); let mockDateNow; beforeEach(() => { From d8adad13c947ea498026bf3d6e8a85af9857a295 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Wed, 25 Mar 2020 18:12:31 -0400 Subject: [PATCH 05/18] thanks, yoda... --- web/src/components/SaveMessage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/SaveMessage.js b/web/src/components/SaveMessage.js index 592cfca627..6bd919571e 100644 --- a/web/src/components/SaveMessage.js +++ b/web/src/components/SaveMessage.js @@ -57,13 +57,13 @@ class SaveMessage extends React.Component { // eslint's "yoda": "Expected literal to be on the right side of <=" // Which is easier to visualize on a number line, Mr. Yoda? // lowerBound <= object.value() && object.value() < upperBound // or... - if (duration.asMinutes() <= 1 && duration.asDays() < 1) { + if (duration.asMinutes() >= 1 && duration.asDays() < 1) { result = `${result} ${lastSavedMoment.format("hh:mm a")}`; } - if (duration.asDays() <= 1 && duration.asYears() < 1) { + if (duration.asDays() >= 1 && duration.asYears() < 1) { result = `${result} ${lastSavedMoment.format("MMMM D")}`; } - if (duration.asYears() <= 1) { + if (duration.asYears() >= 1) { result = `${result} ${lastSavedMoment.format("MMMM D, YYYY")}`; } result = `${result} (${lastSavedMoment.fromNow()})`; From 8f932be310930b063d296e9dc5af81a3a1c89678 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Wed, 25 Mar 2020 18:13:09 -0400 Subject: [PATCH 06/18] update Header test and snapshot --- web/src/components/Header.test.js | 4 ++-- web/src/components/__snapshots__/Header.test.js.snap | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/web/src/components/Header.test.js b/web/src/components/Header.test.js index 782897b25c..b2a80e39ad 100644 --- a/web/src/components/Header.test.js +++ b/web/src/components/Header.test.js @@ -139,7 +139,7 @@ describe('Header component', () => { }} isAdmin={false} isSaving={false} - lastSaved="last save date" + lastSaved="2020-01-01T12:00:00.000Z" pushRoute={() => {}} showSiteTitle={false} /> @@ -159,7 +159,7 @@ describe('Header component', () => { }} isAdmin={false} isSaving - lastSaved="last save date" + lastSaved="2020-01-01T17:00:00.000Z" pushRoute={() => {}} showSiteTitle={false} /> diff --git a/web/src/components/__snapshots__/Header.test.js.snap b/web/src/components/__snapshots__/Header.test.js.snap index 150254ea37..f77b8b5bb0 100644 --- a/web/src/components/__snapshots__/Header.test.js.snap +++ b/web/src/components/__snapshots__/Header.test.js.snap @@ -605,8 +605,10 @@ exports[`Header component renders the state user home title when a state user is - Saved - last save date + + From 063f850034b3ab5b8823d0a75a69ff39d90f8377 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Thu, 26 Mar 2020 11:23:41 -0400 Subject: [PATCH 07/18] working auto-update test --- web/src/components/SaveMessage.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index 7f43f2d8fa..e58ffe3ff8 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -24,6 +24,25 @@ describe("", () => { }); }); + describe("when observed saved time changes from 59.500s to 1m", () => { + it('auto-updates', (done) => { + lastSaved = moment().subtract(59500, "milliseconds"); + subject = shallow( + + ); + expect(subject.text()).toMatch("Saved"); + + setTimeout(() => { + try { + expect(subject.text()).toMatch(/\(1 minute ago\)$/); + done(); + } catch (e) { + done.fail(e); + } + }, 1000); + }) + }) + describe("given current time is January 1, 2020 12:00 pm", () => { const jan1AtNoon = new Date(2020, 0, 1, 12, 0); let mockDateNow; From 69e65a87b62a143d3c245b69beda68f7bda7d6de Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Thu, 26 Mar 2020 11:26:10 -0400 Subject: [PATCH 08/18] add dem semicolons --- web/src/components/SaveMessage.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index e58ffe3ff8..012ba90201 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -40,8 +40,8 @@ describe("", () => { done.fail(e); } }, 1000); - }) - }) + }); + }); describe("given current time is January 1, 2020 12:00 pm", () => { const jan1AtNoon = new Date(2020, 0, 1, 12, 0); From 99421c4fd85106fb0b5b1bc026ce603e498e6fa0 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Thu, 26 Mar 2020 11:42:48 -0400 Subject: [PATCH 09/18] ugly auto-update test, but it seems to work --- web/src/components/SaveMessage.test.js | 35 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index 012ba90201..365d1dbcba 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -24,22 +24,31 @@ describe("", () => { }); }); - describe("when observed saved time changes from 59.500s to 1m", () => { - it('auto-updates', (done) => { - lastSaved = moment().subtract(59500, "milliseconds"); + describe("when observed saved time changes to 1 minute ago", () => { + const now = new Date(2020, 0, 1, 12, 0); + const oneMinuteFromNow = new Date(2020, 0, 1, 12, 1); + let mockDateNow; + + beforeEach(() => { + jest.useFakeTimers(); + mockDateNow = jest + .spyOn(Date, "now") + .mockReturnValueOnce(now) + .mockReturnValue(oneMinuteFromNow); + }); + + afterEach(() => { + mockDateNow.mockRestore(); + jest.clearAllTimers(); + }); + + it('auto-updates', () => { subject = shallow( - + ); expect(subject.text()).toMatch("Saved"); - - setTimeout(() => { - try { - expect(subject.text()).toMatch(/\(1 minute ago\)$/); - done(); - } catch (e) { - done.fail(e); - } - }, 1000); + jest.advanceTimersByTime(1000); + expect(subject.text()).toMatch(/\(1 minute ago\)$/); }); }); From ce556c75ef8a506aa1bff10f6acc17011561bcd3 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Thu, 26 Mar 2020 14:51:47 -0400 Subject: [PATCH 10/18] refactor tests --- web/src/components/SaveMessage.test.js | 84 ++++---------------------- 1 file changed, 13 insertions(+), 71 deletions(-) diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index 365d1dbcba..5232450d42 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -42,7 +42,7 @@ describe("", () => { jest.clearAllTimers(); }); - it('auto-updates', () => { + it('auto-updates from "Saved" to (1 minute ago)', () => { subject = shallow( ); @@ -66,79 +66,21 @@ describe("", () => { mockDateNow.mockRestore(); }); - describe("when saved 1 minute ago", () => { - it('displays "Last saved 11:59 am (1 minute ago)"', () => { - lastSaved = moment(jan1AtNoon).subtract(1, "minutes"); - subject = shallow( - - ); - expect(subject.text()).toMatch(/^Last saved/); - expect(subject.text()).toMatch(/11:59 am/); - expect(subject.text()).toMatch(/\(1 minute ago\)$/); - }); - }); - - describe("when saved 23 hours and 59 minutes ago", () => { - it('displays "Last saved 12:01 pm (1 day ago)"', () => { - lastSaved = moment(jan1AtNoon) - .subtract(23, "hours") - .subtract(59, "minutes"); - subject = shallow( - - ); - expect(subject.text()).toMatch(/^Last saved/); - expect(subject.text()).toMatch(/12:01 pm/); - expect(subject.text()).toMatch(/\(1 day ago\)$/); - }); - }); - - describe("when saved 1 day ago", () => { - it('displays "Last saved December 30 (1 day ago)"', () => { - lastSaved = moment().subtract(1, "day"); - subject = shallow( - - ); - expect(subject.text()).toMatch(/^Last saved/); - expect(subject.text()).toMatch(/December 31/); - expect(subject.text()).toMatch(/\(1 day ago\)$/); - }); - }); - - describe("when saved 30 days ago", () => { - it('displays "Last saved December 2 (1 month ago)"', () => { - lastSaved = moment().subtract(30, "day"); - subject = shallow( - - ); - expect(subject.text()).toMatch(/^Last saved/); - expect(subject.text()).toMatch(/December 2/); - expect(subject.text()).not.toMatch(/2019/); - expect(subject.text()).toMatch(/\(1 month ago\)$/); - }); - }); - - describe("when saved 364 days ago", () => { - it('displays "Last saved January 2 (1 year ago)"', () => { - lastSaved = moment().subtract(364, "day"); - subject = shallow( - - ); - expect(subject.text()).toMatch(/^Last saved/); - expect(subject.text()).toMatch(/January 2/); - expect(subject.text()).not.toMatch(/2019/); - expect(subject.text()).toMatch(/\(1 year ago\)$/); - }); - }); - - describe("when saved 3 years ago", () => { - it('displays "Last saved January 1, 2017 (3 years ago)"', () => { - lastSaved = moment().subtract(3, "years"); + [ + [1, "minute", "Last saved 11:59 am (1 minute ago)"], + [60 * 24 - 1, "minutes", "Last saved 12:01 pm (1 day ago)"], + [1, "day", "Last saved December 31 (1 day ago)"], + [30, "days", "Last saved December 2 (1 month ago)"], + [364, "days", "Last saved January 2 (1 year ago)"], + [3, "years", "Last saved January 1, 2017 (3 years ago)"], + ].forEach(([value, timeUnit, result]) => { + let testName = `when saved ${value} ${timeUnit} ago, it displays "${result}"` + test(testName, () => { + lastSaved = moment().subtract(value, timeUnit); subject = shallow( ); - expect(subject.text()).toMatch(/^Last saved/); - expect(subject.text()).toMatch(/January 1, 2017/); - expect(subject.text()).toMatch(/\(3 years ago\)$/); + expect(subject.text()).toEqual(result); }); }); }); From 4356bbdbb0405a6be30e93b773795371ec8ae2f9 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Thu, 26 Mar 2020 16:47:02 -0400 Subject: [PATCH 11/18] remove leading zero from hour --- web/src/components/SaveMessage.js | 2 +- web/src/components/SaveMessage.test.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/components/SaveMessage.js b/web/src/components/SaveMessage.js index 6bd919571e..d5ce35e918 100644 --- a/web/src/components/SaveMessage.js +++ b/web/src/components/SaveMessage.js @@ -58,7 +58,7 @@ class SaveMessage extends React.Component { // Which is easier to visualize on a number line, Mr. Yoda? // lowerBound <= object.value() && object.value() < upperBound // or... if (duration.asMinutes() >= 1 && duration.asDays() < 1) { - result = `${result} ${lastSavedMoment.format("hh:mm a")}`; + result = `${result} ${lastSavedMoment.format("h:mm a")}`; } if (duration.asDays() >= 1 && duration.asYears() < 1) { result = `${result} ${lastSavedMoment.format("MMMM D")}`; diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index 5232450d42..ff00835ed3 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -69,12 +69,13 @@ describe("", () => { [ [1, "minute", "Last saved 11:59 am (1 minute ago)"], [60 * 24 - 1, "minutes", "Last saved 12:01 pm (1 day ago)"], + [3, "hours", "Last saved 9:00 am (3 hours ago)"], [1, "day", "Last saved December 31 (1 day ago)"], [30, "days", "Last saved December 2 (1 month ago)"], [364, "days", "Last saved January 2 (1 year ago)"], [3, "years", "Last saved January 1, 2017 (3 years ago)"], ].forEach(([value, timeUnit, result]) => { - let testName = `when saved ${value} ${timeUnit} ago, it displays "${result}"` + const testName = `when saved ${value} ${timeUnit} ago, it displays "${result}"` test(testName, () => { lastSaved = moment().subtract(value, timeUnit); subject = shallow( From 022adc8587f28cb5e0603018c5b41b5f82ee2dd1 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Fri, 27 Mar 2020 10:10:06 -0400 Subject: [PATCH 12/18] address feedback --- web/src/components/SaveMessage.js | 29 +++++++++++--------------- web/src/components/SaveMessage.test.js | 16 ++++++-------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/web/src/components/SaveMessage.js b/web/src/components/SaveMessage.js index d5ce35e918..f5cc9d5155 100644 --- a/web/src/components/SaveMessage.js +++ b/web/src/components/SaveMessage.js @@ -50,24 +50,19 @@ class SaveMessage extends React.Component { const lastSavedMoment = moment(lastSaved); const difference = currentMoment.diff(lastSavedMoment); const duration = moment.duration(difference); - let result = "Last saved"; + let result = "Last saved "; if (duration.asMinutes() < 1) return "Saved"; - // eslint's "yoda": "Expected literal to be on the right side of <=" - // Which is easier to visualize on a number line, Mr. Yoda? - // lowerBound <= object.value() && object.value() < upperBound // or... - if (duration.asMinutes() >= 1 && duration.asDays() < 1) { - result = `${result} ${lastSavedMoment.format("h:mm a")}`; + if (duration.asDays() < 1) { + result += lastSavedMoment.format("h:mm a"); + } else if (duration.asYears() < 1) { + result += lastSavedMoment.format("MMMM D"); + } else { + result += lastSavedMoment.format("MMMM D, YYYY"); } - if (duration.asDays() >= 1 && duration.asYears() < 1) { - result = `${result} ${lastSavedMoment.format("MMMM D")}`; - } - if (duration.asYears() >= 1) { - result = `${result} ${lastSavedMoment.format("MMMM D, YYYY")}`; - } - result = `${result} (${lastSavedMoment.fromNow()})`; + result += ` (${lastSavedMoment.fromNow()})`; return result; } } @@ -77,11 +72,11 @@ SaveMessage.propTypes = { PropTypes.instanceOf(Date), PropTypes.instanceOf(moment), PropTypes.string - ].isRequred), + ]).isRequired, }; -SaveMessage.defaultProps = { - lastSaved: moment(), -}; +// SaveMessage.defaultProps = { +// lastSaved: moment(), +// }; export default SaveMessage; diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index ff00835ed3..a68f826614 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -4,9 +4,6 @@ import moment from "moment"; import SaveMessage from "./SaveMessage"; describe("", () => { - let lastSaved; - let subject; - describe('when saved less than 1 minute ago, it displays "Saved"', () => { [ ["1 second ago", 1], @@ -15,8 +12,8 @@ describe("", () => { ["59 seconds", 59], ].forEach(([testName, seconds]) => { test(testName, () => { - lastSaved = moment().subtract(seconds, "seconds"); - subject = shallow( + const lastSaved = moment().subtract(seconds, "seconds"); + const subject = shallow( ); expect(subject.text()).toEqual("Saved"); @@ -43,7 +40,7 @@ describe("", () => { }); it('auto-updates from "Saved" to (1 minute ago)', () => { - subject = shallow( + const subject = shallow( ); expect(subject.text()).toMatch("Saved"); @@ -75,10 +72,9 @@ describe("", () => { [364, "days", "Last saved January 2 (1 year ago)"], [3, "years", "Last saved January 1, 2017 (3 years ago)"], ].forEach(([value, timeUnit, result]) => { - const testName = `when saved ${value} ${timeUnit} ago, it displays "${result}"` - test(testName, () => { - lastSaved = moment().subtract(value, timeUnit); - subject = shallow( + test(`when saved ${value} ${timeUnit} ago, it displays "${result}"`, () => { + const lastSaved = moment().subtract(value, timeUnit); + const subject = shallow( ); expect(subject.text()).toEqual(result); From b010620cee32392bbfcc857836a63bcde867e7dd Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Fri, 27 Mar 2020 11:22:36 -0400 Subject: [PATCH 13/18] refactor into functional component, auto-update test broken :( --- web/src/components/SaveMessage.js | 66 ++++++++------------------ web/src/components/SaveMessage.test.js | 28 ++++++----- 2 files changed, 37 insertions(+), 57 deletions(-) diff --git a/web/src/components/SaveMessage.js b/web/src/components/SaveMessage.js index f5cc9d5155..b4bb98fb95 100644 --- a/web/src/components/SaveMessage.js +++ b/web/src/components/SaveMessage.js @@ -1,5 +1,5 @@ import moment from "moment"; -import React from "react"; +import React, { useEffect, useState } from "react"; import PropTypes from 'prop-types'; // Configure moment to display '1 time-unit ago' instead of 'a time-unit ago' @@ -20,52 +20,32 @@ moment.updateLocale("en", { }, }); -class SaveMessage extends React.Component { - constructor(props) { - super(props); +const SaveMessage = ({ lastSaved }) => { + const [currentMoment, setCurrentMoment] = useState(() => moment()); - this.state = { - currentMoment: moment(), - }; - } + useEffect(() => { + let timerID = setInterval(() => setCurrentMoment(moment()), 1000); + return () => clearInterval(timerID); + }); - componentDidMount() { - this.timerID = setInterval(() => this.updateClock(), 1000); - } + const lastSavedMoment = moment(lastSaved); + const difference = currentMoment.diff(lastSavedMoment); + const duration = moment.duration(difference); + let result = "Last saved "; - componentWillUnmount() { - clearInterval(this.timerID); - } + if (duration.asMinutes() < 1) return "Saved"; - updateClock() { - this.setState({ - currentMoment: moment(), - }); + if (duration.asDays() < 1) { + result += lastSavedMoment.format("h:mm a"); + } else if (duration.asYears() < 1) { + result += lastSavedMoment.format("MMMM D"); + } else { + result += lastSavedMoment.format("MMMM D, YYYY"); } - render() { - const { lastSaved } = this.props; - const { currentMoment } = this.state; - - const lastSavedMoment = moment(lastSaved); - const difference = currentMoment.diff(lastSavedMoment); - const duration = moment.duration(difference); - let result = "Last saved "; - - if (duration.asMinutes() < 1) return "Saved"; - - if (duration.asDays() < 1) { - result += lastSavedMoment.format("h:mm a"); - } else if (duration.asYears() < 1) { - result += lastSavedMoment.format("MMMM D"); - } else { - result += lastSavedMoment.format("MMMM D, YYYY"); - } - - result += ` (${lastSavedMoment.fromNow()})`; - return result; - } -} + result += ` (${lastSavedMoment.fromNow()})`; + return result; +}; SaveMessage.propTypes = { lastSaved: PropTypes.oneOfType([ @@ -75,8 +55,4 @@ SaveMessage.propTypes = { ]).isRequired, }; -// SaveMessage.defaultProps = { -// lastSaved: moment(), -// }; - export default SaveMessage; diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index a68f826614..e7e618c8c9 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -1,5 +1,6 @@ import React from "react"; -import { shallow } from "enzyme"; +import { act } from 'react-dom/test-utils'; +import { mount, shallow } from "enzyme"; import moment from "moment"; import SaveMessage from "./SaveMessage"; @@ -21,30 +22,33 @@ describe("", () => { }); }); - describe("when observed saved time changes to 1 minute ago", () => { - const now = new Date(2020, 0, 1, 12, 0); - const oneMinuteFromNow = new Date(2020, 0, 1, 12, 1); - let mockDateNow; + xdescribe("when observed saved time changes to 1 minute ago", () => { + // const now = new Date(2020, 0, 1, 12, 0); + // const oneMinuteFromNow = new Date(2020, 0, 1, 12, 1); + // let mockDateNow; beforeEach(() => { jest.useFakeTimers(); - mockDateNow = jest - .spyOn(Date, "now") - .mockReturnValueOnce(now) - .mockReturnValue(oneMinuteFromNow); + // mockDateNow = jest + // .spyOn(Date, "now") + // .mockReturnValueOnce(now) + // .mockReturnValueOnce(now) + // .mockReturnValue(oneMinuteFromNow); }); afterEach(() => { - mockDateNow.mockRestore(); + // mockDateNow.mockRestore(); jest.clearAllTimers(); }); it('auto-updates from "Saved" to (1 minute ago)', () => { const subject = shallow( - + ); expect(subject.text()).toMatch("Saved"); - jest.advanceTimersByTime(1000); + jest.advanceTimersByTime(65*1000); + // subject.setProps(); + act(() => {subject.setProps()}); expect(subject.text()).toMatch(/\(1 minute ago\)$/); }); }); From 017b419094950f0e10165f5294b8524f42dddf60 Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Fri, 27 Mar 2020 11:53:14 -0400 Subject: [PATCH 14/18] use react-test-renderer for testing --- web/src/components/SaveMessage.test.js | 57 +++++++++++++------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/web/src/components/SaveMessage.test.js b/web/src/components/SaveMessage.test.js index e7e618c8c9..5be2c01dd0 100644 --- a/web/src/components/SaveMessage.test.js +++ b/web/src/components/SaveMessage.test.js @@ -1,10 +1,11 @@ import React from "react"; -import { act } from 'react-dom/test-utils'; -import { mount, shallow } from "enzyme"; +import { create, act } from "react-test-renderer"; import moment from "moment"; import SaveMessage from "./SaveMessage"; describe("", () => { + let subject; + describe('when saved less than 1 minute ago, it displays "Saved"', () => { [ ["1 second ago", 1], @@ -14,42 +15,42 @@ describe("", () => { ].forEach(([testName, seconds]) => { test(testName, () => { const lastSaved = moment().subtract(seconds, "seconds"); - const subject = shallow( - - ); - expect(subject.text()).toEqual("Saved"); + // https://reactjs.org/docs/test-renderer.html#testrendereract + act(() => { + subject = create(); + }) + expect(subject.toJSON()).toEqual("Saved"); }); }); }); - xdescribe("when observed saved time changes to 1 minute ago", () => { - // const now = new Date(2020, 0, 1, 12, 0); - // const oneMinuteFromNow = new Date(2020, 0, 1, 12, 1); - // let mockDateNow; + describe("when observed saved time changes to 1 minute ago", () => { + const now = new Date(2020, 0, 1, 12, 0); + const oneMinuteFromNow = new Date(2020, 0, 1, 12, 1); + let mockDateNow; beforeEach(() => { jest.useFakeTimers(); - // mockDateNow = jest - // .spyOn(Date, "now") - // .mockReturnValueOnce(now) - // .mockReturnValueOnce(now) - // .mockReturnValue(oneMinuteFromNow); + mockDateNow = jest + .spyOn(Date, "now") + .mockReturnValueOnce(now) + .mockReturnValueOnce(now) + .mockReturnValueOnce(now) + .mockReturnValue(oneMinuteFromNow); }); afterEach(() => { - // mockDateNow.mockRestore(); + mockDateNow.mockRestore(); jest.clearAllTimers(); }); it('auto-updates from "Saved" to (1 minute ago)', () => { - const subject = shallow( - - ); - expect(subject.text()).toMatch("Saved"); - jest.advanceTimersByTime(65*1000); - // subject.setProps(); - act(() => {subject.setProps()}); - expect(subject.text()).toMatch(/\(1 minute ago\)$/); + act(() => { + subject = create(); + }) + expect(subject.toJSON()).toMatch("Saved"); + act(() => jest.advanceTimersByTime(60 * 1000)); + expect(subject.toJSON()).toMatch(/\(1 minute ago\)$/); }); }); @@ -78,10 +79,10 @@ describe("", () => { ].forEach(([value, timeUnit, result]) => { test(`when saved ${value} ${timeUnit} ago, it displays "${result}"`, () => { const lastSaved = moment().subtract(value, timeUnit); - const subject = shallow( - - ); - expect(subject.text()).toEqual(result); + act(() => { + subject = create(); + }) + expect(subject.toJSON()).toEqual(result); }); }); }); From f350adc4f68adc079c2fb57065e4d257549e204e Mon Sep 17 00:00:00 2001 From: Richard Davis Date: Fri, 27 Mar 2020 11:53:24 -0400 Subject: [PATCH 15/18] update snapshots --- .../__snapshots__/CardForm.test.js.snap | 13 --- .../__snapshots__/ConsentBanner.test.js.snap | 5 - .../DashboardButton.test.js.snap | 1 - .../__snapshots__/DateField.test.js.snap | 14 +++ .../__snapshots__/DollarField.test.js.snap | 110 ++++++++---------- .../FormAndReviewList.test.js.snap | 14 --- .../__snapshots__/Review.test.js.snap | 8 -- .../__snapshots__/ApdExport.test.js.snap | 1 - .../activity/__snapshots__/All.test.js.snap | 1 - .../__snapshots__/EntryDetails.test.js.snap | 9 -- .../__snapshots__/MyAccount.test.js.snap | 6 - .../__snapshots__/Export.test.js.snap | 1 - 12 files changed, 64 insertions(+), 119 deletions(-) diff --git a/web/src/components/__snapshots__/CardForm.test.js.snap b/web/src/components/__snapshots__/CardForm.test.js.snap index c67df7aec8..bbaebb411f 100644 --- a/web/src/components/__snapshots__/CardForm.test.js.snap +++ b/web/src/components/__snapshots__/CardForm.test.js.snap @@ -33,7 +33,6 @@ exports[`card form wrapper disables the save button if canSubmit is false 1`] = className="ds-u-margin-top--5" >