Skip to content

Commit

Permalink
Add SaveMessage component to Header (#2133)
Browse files Browse the repository at this point in the history
* add SaveMessage component to Header

* preserve space between Check and SaveMessage components

* use moment#duration to calculate time differences, use expect#toMatch for better errors

* appease the demands of our linter

* thanks, yoda...

* update Header test and snapshot

* working auto-update test

* add dem semicolons

* ugly auto-update test, but it seems to work

* refactor tests

* remove leading zero from hour

* address feedback

* refactor into functional component, auto-update test broken :(

* use react-test-renderer for testing

* update snapshots

* appease the linter

* update snapshots

* update changelog
  • Loading branch information
radavis committed Mar 31, 2020
1 parent 1ee88d0 commit 41fba39
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 6 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Anticipated release: April 6, 2020

#### 🚀 New features

- The header displays the time ago since the APD was last saved ([#2104])

#### 🐛 Bugs fixed

#### ⚙️ Behind the scenes
Expand All @@ -12,4 +14,6 @@ Anticipated release: April 6, 2020

# Previous releases

See our [release history](https://github.com/18F/cms-hitech-apd/releases)
See our [release history](https://github.com/18F/cms-hitech-apd/releases)

[#2104]: https://github.com/18F/cms-hitech-apd/issues/2104
3 changes: 2 additions & 1 deletion web/src/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -86,7 +87,7 @@ class Header extends Component {
</span>
) : (
<span>
<Check /> Saved {lastSaved}
<Check /> <SaveMessage lastSaved={lastSaved} />
</span>
)}
</span>
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/Header.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
Expand All @@ -159,7 +159,7 @@ describe('Header component', () => {
}}
isAdmin={false}
isSaving
lastSaved="last save date"
lastSaved="2020-01-01T17:00:00.000Z"
pushRoute={() => {}}
showSiteTitle={false}
/>
Expand Down
58 changes: 58 additions & 0 deletions web/src/components/SaveMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import moment from "moment";
import { useEffect, useState } 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
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",
},
});

const SaveMessage = ({ lastSaved }) => {
const [currentMoment, setCurrentMoment] = useState(() => moment());

useEffect(() => {
const timerID = setInterval(() => setCurrentMoment(moment()), 1000);
return () => clearInterval(timerID);
});

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;
};

SaveMessage.propTypes = {
lastSaved: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.instanceOf(moment),
PropTypes.string
]).isRequired,
};

export default SaveMessage;
89 changes: 89 additions & 0 deletions web/src/components/SaveMessage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from "react";
import { create, act } from "react-test-renderer";
import moment from "moment";
import SaveMessage from "./SaveMessage";

describe("<SaveMessage />", () => {
let 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, () => {
const lastSaved = moment().subtract(seconds, "seconds");
// https://reactjs.org/docs/test-renderer.html#testrendereract
act(() => {
subject = create(<SaveMessage lastSaved={lastSaved} />);
})
expect(subject.toJSON()).toEqual("Saved");
});
});
});

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)
.mockReturnValueOnce(now)
.mockReturnValue(oneMinuteFromNow);
});

afterEach(() => {
mockDateNow.mockRestore();
jest.clearAllTimers();
});

it('auto-updates from "Saved" to (1 minute ago)', () => {
act(() => {
subject = create(<SaveMessage lastSaved={now} />);
})
expect(subject.toJSON()).toMatch("Saved");
act(() => jest.advanceTimersByTime(60 * 1000));
expect(subject.toJSON()).toMatch(/\(1 minute ago\)$/);
});
});

describe("given current time is January 1, 2020 12:00 pm", () => {
const jan1AtNoon = new Date(2020, 0, 1, 12, 0);
let mockDateNow;

beforeEach(() => {
mockDateNow = jest
.spyOn(Date, "now")
.mockReturnValue(jan1AtNoon.getTime());
});

afterEach(() => {
mockDateNow.mockRestore();
});

[
[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]) => {
test(`when saved ${value} ${timeUnit} ago, it displays "${result}"`, () => {
const lastSaved = moment().subtract(value, timeUnit);
act(() => {
subject = create(<SaveMessage lastSaved={lastSaved} />);
})
expect(subject.toJSON()).toEqual(result);
});
});
});
});
6 changes: 4 additions & 2 deletions web/src/components/__snapshots__/Header.test.js.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 41fba39

Please sign in to comment.