Commit 5957b12d authored by Hannah Stepanek's avatar Hannah Stepanek

Cleanup util time tests

- Use now variable instead of walking the clock.
- Use iso format instead of seconds.
- Pass a Date object instead of an iso format to time.<methods>so
the methods off the Date object can be mocked in order to avoid
timezone specific tests. Since methods like getFullYear output the
year in whatever timezone the operating system is set to, these
methods must be mocked/mapped to their UTC equivalents when testing
such as getUTCFullYear.
- Remove offseting of time based on timezone as this is now covered
by mocking the getFullYear method and not needed for the decayInterval
as that only cares about time deltas.
parent fee4bac3
...@@ -17,10 +17,10 @@ function Timestamp({ className, href, timestamp }) { ...@@ -17,10 +17,10 @@ function Timestamp({ className, href, timestamp }) {
const [now, setNow] = useState(new Date()); const [now, setNow] = useState(new Date());
// Fuzzy, relative timestamp (eg. '6 days ago') // Fuzzy, relative timestamp (eg. '6 days ago')
const relativeTimestamp = useMemo(() => toFuzzyString(timestamp, now), [ const relativeTimestamp = useMemo(
timestamp, () => toFuzzyString(timestamp ? new Date(timestamp) : null, now),
now, [timestamp, now]
]); );
// Absolute timestamp (eg. 'Tue 22nd Dec 2015, 16:00') // Absolute timestamp (eg. 'Tue 22nd Dec 2015, 16:00')
const absoluteTimestamp = useMemo(() => formatDate(new Date(timestamp)), [ const absoluteTimestamp = useMemo(() => formatDate(new Date(timestamp)), [
......
...@@ -5,128 +5,168 @@ const time = require('../time'); ...@@ -5,128 +5,168 @@ const time = require('../time');
const minute = 60; const minute = 60;
const hour = minute * 60; const hour = minute * 60;
const day = hour * 24; const day = hour * 24;
const month = day * 30; const msPerSecond = 1000;
const year = day * 365;
const FIXTURES_TO_FUZZY_STRING = [
[10, 'Just now'],
[29, 'Just now'],
[49, '49 secs'],
[minute + 5, '1 min'],
[3 * minute + 5, '3 mins'],
[hour, '1 hr'],
[4 * hour, '4 hrs'],
[27 * hour, '1 Jan'],
[3 * day + 30 * minute, '1 Jan'],
[6 * month + 2 * day, '1 Jan'],
[1 * year, '1 Jan 1970'],
[1 * year + 2 * month, '1 Jan 1970'],
[2 * year, '1 Jan 1970'],
[8 * year, '1 Jan 1970'],
];
const FIXTURES_NEXT_FUZZY_UPDATE = [
[10, 5], // we have a minimum of 5 secs
[29, 5],
[49, 5],
[minute + 5, minute],
[3 * minute + 5, minute],
[4 * hour, hour],
[27 * hour, null],
[3 * day + 30 * minute, null],
[6 * month + 2 * day, null],
[8 * year, null],
];
describe('sidebar.util.time', function() { describe('sidebar.util.time', function() {
let sandbox; let sandbox;
let fakeIntl;
beforeEach(function() { beforeEach(function() {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
sandbox.useFakeTimers(); sandbox.useFakeTimers();
// Ensure that the current local date is 01/01/1970, as this is assumed by fakeIntl = {
// test expectations DateTimeFormat: sinon.stub().returns({
const offset = new Date().getTimezoneOffset(); format: sinon.stub(),
if (offset > 0) { }),
sandbox.clock.tick(offset * 60 * 1000); };
} // Clear the formatters cache so that mocked formatters
// from one test run don't affect the next.
time.clearFormatters();
}); });
afterEach(function() { afterEach(function() {
sandbox.restore(); sandbox.restore();
}); });
describe('.toFuzzyString', function() { const fakeDate = isoString => {
function mockIntl() { // Since methods like Date.getFullYear output the year in
return { // whatever timezone the node timezone is set to, these
DateTimeFormat: function() { // methods must be mocked/mapped to their UTC equivalents when
return { // testing such as getUTCFullYear in order to have timezone
format: function() { // agnostic tests.
if (new Date().getYear() === 70) { // Example:
return '1 Jan'; // An annotation was posted at 2019-01-01T01:00:00 UTC and now the
} else { // current date is a few days later; 2019-01-10.
return '1 Jan 1970'; // - A user in the UK who views the annotation will see “Jan 1”
} // on the annotation card (correct)
}, // - A user in San Francisco who views the annotation will see
// “Dec 31st 2018" on the annotation card (also correct from
// their point of view).
const date = new Date(isoString);
date.getFullYear = sinon.stub().returns(date.getUTCFullYear());
return date;
}; };
},
};
}
describe('.toFuzzyString', function() {
it('Handles empty dates', function() { it('Handles empty dates', function() {
const t = null; const date = null;
const expect = ''; const expect = '';
assert.equal(time.toFuzzyString(t, undefined, mockIntl()), expect); assert.equal(time.toFuzzyString(date, undefined), expect);
}); });
const testFixture = function(f) { [
return function() { { now: '1970-01-01T00:00:10.000Z', text: 'Just now' },
const t = new Date().toISOString(); { now: '1970-01-01T00:00:29.000Z', text: 'Just now' },
const expect = f[1]; { now: '1970-01-01T00:00:49.000Z', text: '49 secs' },
sandbox.clock.tick(f[0] * 1000); { now: '1970-01-01T00:01:05.000Z', text: '1 min' },
assert.equal(time.toFuzzyString(t, undefined, mockIntl()), expect); { now: '1970-01-01T00:03:05.000Z', text: '3 mins' },
}; { now: '1970-01-01T01:00:00.000Z', text: '1 hr' },
}; { now: '1970-01-01T04:00:00.000Z', text: '4 hrs' },
].forEach(test => {
it('creates correct fuzzy string for fixture ' + test.now, () => {
const timeStamp = fakeDate('1970-01-01T00:00:00.000Z');
const now = fakeDate(test.now);
assert.equal(time.toFuzzyString(timeStamp, now), test.text);
});
});
for (let i = 0, f; i < FIXTURES_TO_FUZZY_STRING.length; i++) { [
f = FIXTURES_TO_FUZZY_STRING[i]; {
it('creates correct fuzzy string for fixture ' + i, testFixture(f)); now: '1970-01-02T03:00:00.000Z',
text: '2 Jan',
options: { day: 'numeric', month: 'short' },
},
{
now: '1970-01-04T00:30:00.000Z',
text: '4 Jan',
options: { day: 'numeric', month: 'short' },
},
{
now: '1970-07-03T00:00:00.000Z',
text: '3 July',
options: { day: 'numeric', month: 'short' },
},
{
now: '1971-01-01T00:00:00.000Z',
text: '1 Jan 1970',
options: { day: 'numeric', month: 'short', year: 'numeric' },
},
{
now: '1971-03-01T00:00:00.000Z',
text: '1 Jan 1970',
options: { day: 'numeric', month: 'short', year: 'numeric' },
},
{
now: '1972-01-01T00:00:00.000Z',
text: '1 Jan 1970',
options: { day: 'numeric', month: 'short', year: 'numeric' },
},
{
now: '1978-01-01T00:00:00.000Z',
text: '1 Jan 1970',
options: { day: 'numeric', month: 'short', year: 'numeric' },
},
].forEach(test => {
it(
'passes correct arguments to `Intl.DateTimeFormat.format` for fixture ' +
test.now,
() => {
const timeStamp = fakeDate('1970-01-01T00:00:00.000Z');
const now = fakeDate(test.now);
fakeIntl.DateTimeFormat().format.returns(test.text); // eslint-disable-line new-cap
assert.equal(time.toFuzzyString(timeStamp, now, fakeIntl), test.text);
assert.calledWith(fakeIntl.DateTimeFormat, undefined, test.options);
assert.calledWith(fakeIntl.DateTimeFormat().format, timeStamp); // eslint-disable-line new-cap
} }
);
});
it('falls back to simple strings for >24hrs ago', function() { it('falls back to simple strings for >24hrs ago', function() {
// If window.Intl is not available then the date formatting for dates // If window.Intl is not available then the date formatting for dates
// more than one day ago falls back to a simple date string. // more than one day ago falls back to a simple date string.
const d = new Date().toISOString(); const timeStamp = fakeDate('1970-01-01T00:00:00.000Z');
sandbox.clock.tick(day * 2 * 1000); timeStamp.toDateString = sinon.stub().returns('Thu Jan 01 1970');
const now = fakeDate('1970-01-02T00:00:00.000Z');
assert.equal(time.toFuzzyString(d, undefined, null), 'Thu Jan 01 1970'); const formattedDate = time.toFuzzyString(timeStamp, now, null);
assert.calledOnce(timeStamp.toDateString);
assert.equal(formattedDate, 'Thu Jan 01 1970');
}); });
it('falls back to simple strings for >1yr ago', function() { it('falls back to simple strings for >1yr ago', function() {
// If window.Intl is not available then the date formatting for dates // If window.Intl is not available then the date formatting for dates
// more than one year ago falls back to a simple date string. // more than one year ago falls back to a simple date string.
const d = new Date().toISOString(); const timeStamp = fakeDate('1970-01-01T00:00:00.000Z');
sandbox.clock.tick(year * 2 * 1000); timeStamp.toDateString = sinon.stub().returns('Thu Jan 01 1970');
const now = fakeDate('1972-01-01T00:00:00.000Z');
assert.equal(time.toFuzzyString(d, undefined, null), 'Thu Jan 01 1970'); const formattedDate = time.toFuzzyString(timeStamp, now, null);
assert.calledOnce(timeStamp.toDateString);
assert.equal(formattedDate, 'Thu Jan 01 1970');
}); });
}); });
describe('.decayingInterval', function() { describe('.decayingInterval', function() {
it('Handles empty dates', function() {
const date = null;
time.decayingInterval(date, undefined);
});
it('uses a short delay for recent timestamps', function() { it('uses a short delay for recent timestamps', function() {
const date = new Date(); const date = new Date().toISOString();
const callback = sandbox.stub(); const callback = sandbox.stub();
time.decayingInterval(date, callback); time.decayingInterval(date, callback);
sandbox.clock.tick(6 * 1000); sandbox.clock.tick(6 * msPerSecond);
assert.calledWith(callback, date); assert.calledWith(callback, date);
sandbox.clock.tick(6 * 1000); sandbox.clock.tick(6 * msPerSecond);
assert.calledTwice(callback); assert.calledTwice(callback);
}); });
it('uses a longer delay for older timestamps', function() { it('uses a longer delay for older timestamps', function() {
const date = new Date(); const date = new Date().toISOString();
const ONE_MINUTE = minute * 1000; const ONE_MINUTE = minute * msPerSecond;
sandbox.clock.tick(10 * ONE_MINUTE); sandbox.clock.tick(10 * ONE_MINUTE);
const callback = sandbox.stub(); const callback = sandbox.stub();
time.decayingInterval(date, callback); time.decayingInterval(date, callback);
...@@ -139,17 +179,17 @@ describe('sidebar.util.time', function() { ...@@ -139,17 +179,17 @@ describe('sidebar.util.time', function() {
}); });
it('returned function cancels the timer', function() { it('returned function cancels the timer', function() {
const date = new Date(); const date = new Date().toISOString();
const callback = sandbox.stub(); const callback = sandbox.stub();
const cancel = time.decayingInterval(date, callback); const cancel = time.decayingInterval(date, callback);
cancel(); cancel();
sandbox.clock.tick(minute * 1000); sandbox.clock.tick(minute * msPerSecond);
assert.notCalled(callback); assert.notCalled(callback);
}); });
it('does not set a timeout for dates > 24hrs ago', function() { it('does not set a timeout for dates > 24hrs ago', function() {
const date = new Date(); const date = new Date().toISOString();
const ONE_DAY = day * 1000; const ONE_DAY = day * msPerSecond;
sandbox.clock.tick(10 * ONE_DAY); sandbox.clock.tick(10 * ONE_DAY);
const callback = sandbox.stub(); const callback = sandbox.stub();
...@@ -162,26 +202,31 @@ describe('sidebar.util.time', function() { ...@@ -162,26 +202,31 @@ describe('sidebar.util.time', function() {
describe('.nextFuzzyUpdate', function() { describe('.nextFuzzyUpdate', function() {
it('Handles empty dates', function() { it('Handles empty dates', function() {
const t = null; const date = null;
const expect = null; const expect = null;
assert.equal(time.nextFuzzyUpdate(t), expect); assert.equal(time.nextFuzzyUpdate(date, undefined), expect);
}); });
const testFixture = function(f) { [
return function() { { now: '1970-01-01T00:00:10.000Z', expectedUpdateTime: 5 }, // we have a minimum of 5 secs
const t = new Date().toISOString(); { now: '1970-01-01T00:00:20.000Z', expectedUpdateTime: 5 },
const expect = f[1]; { now: '1970-01-01T00:00:49.000Z', expectedUpdateTime: 5 },
sandbox.clock.tick(f[0] * 1000); { now: '1970-01-01T00:01:05.000Z', expectedUpdateTime: minute },
assert.equal(time.nextFuzzyUpdate(t), expect); { now: '1970-01-01T00:03:05.000Z', expectedUpdateTime: minute },
}; { now: '1970-01-01T04:00:00.000Z', expectedUpdateTime: hour },
}; { now: '1970-01-02T03:00:00.000Z', expectedUpdateTime: null },
{ now: '1970-01-04T00:30:00.000Z', expectedUpdateTime: null },
for (let i = 0, f; i < FIXTURES_NEXT_FUZZY_UPDATE.length; i++) { { now: '1970-07-02T00:00:00.000Z', expectedUpdateTime: null },
f = FIXTURES_NEXT_FUZZY_UPDATE[i]; { now: '1978-01-01T00:00:00.000Z', expectedUpdateTime: null },
it( ].forEach(test => {
'gives correct next fuzzy update time for fixture ' + i, it('gives correct next fuzzy update time for fixture ' + test.now, () => {
testFixture(f) const timeStamp = fakeDate('1970-01-01T00:00:00.000Z');
const now = fakeDate(test.now);
assert.equal(
time.nextFuzzyUpdate(timeStamp, now),
test.expectedUpdateTime
); );
} });
});
}); });
}); });
...@@ -55,8 +55,14 @@ function nHr(date, now) { ...@@ -55,8 +55,14 @@ function nHr(date, now) {
// Cached DateTimeFormat instances, // Cached DateTimeFormat instances,
// because instantiating a DateTimeFormat is expensive. // because instantiating a DateTimeFormat is expensive.
const formatters = {}; let formatters = {};
/**
* Clears the cache of formatters.
*/
function clearFormatters() {
formatters = {};
}
/** /**
* Efficiently return `date` formatted with `options`. * Efficiently return `date` formatted with `options`.
* *
...@@ -139,10 +145,15 @@ const BREAKPOINTS = [ ...@@ -139,10 +145,15 @@ const BREAKPOINTS = [
}, },
]; ];
/**
* Returns a dict that describes how to format the date based on the delta
* between date and now.
*
* @param {Date} date - The date to consider as the timestamp to format.
* @param {Date} now - The date to consider as the current time.
* @return {breakpoint} A dict that describes how to format the date.
*/
function getBreakpoint(date, now) { function getBreakpoint(date, now) {
// Turn the given ISO 8601 string into a Date object.
date = new Date(date);
let breakpoint; let breakpoint;
for (let i = 0; i < BREAKPOINTS.length; i++) { for (let i = 0; i < BREAKPOINTS.length; i++) {
breakpoint = BREAKPOINTS[i]; breakpoint = BREAKPOINTS[i];
...@@ -154,12 +165,12 @@ function getBreakpoint(date, now) { ...@@ -154,12 +165,12 @@ function getBreakpoint(date, now) {
return null; return null;
} }
function nextFuzzyUpdate(date) { function nextFuzzyUpdate(date, now) {
if (!date) { if (!date) {
return null; return null;
} }
let secs = getBreakpoint(date, new Date()).nextUpdate; let secs = getBreakpoint(date, now).nextUpdate;
if (secs === null) { if (secs === null) {
return null; return null;
...@@ -183,12 +194,15 @@ function nextFuzzyUpdate(date) { ...@@ -183,12 +194,15 @@ function nextFuzzyUpdate(date) {
* This can be used to refresh parts of a UI whose * This can be used to refresh parts of a UI whose
* update frequency depends on the age of a timestamp. * update frequency depends on the age of a timestamp.
* *
* @param {String} date - An ISO 8601 date string timestamp to format.
* @param {Date} callback - A callback function to call when the timestamp changes.
* @return {Function} A function that cancels the automatic refresh. * @return {Function} A function that cancels the automatic refresh.
*/ */
function decayingInterval(date, callback) { function decayingInterval(date, callback) {
let timer; let timer;
const timeStamp = date ? new Date(date) : null;
const update = function() { const update = function() {
const fuzzyUpdate = nextFuzzyUpdate(date); const fuzzyUpdate = nextFuzzyUpdate(timeStamp, new Date());
if (fuzzyUpdate === null) { if (fuzzyUpdate === null) {
return; return;
} }
...@@ -208,18 +222,20 @@ function decayingInterval(date, callback) { ...@@ -208,18 +222,20 @@ function decayingInterval(date, callback) {
/** /**
* Formats a date as a string relative to the current date. * Formats a date as a string relative to the current date.
* *
* @param {number} date - The absolute timestamp to format. * @param {Date} date - The date to consider as the timestamp to format.
* @param {Date} [now] - The date to consider as the current time. * @param {Date} now - The date to consider as the current time.
* @param {Object} Intl - JS internationalization API implementation.
* @return {string} A 'fuzzy' string describing the relative age of the date. * @return {string} A 'fuzzy' string describing the relative age of the date.
*/ */
function toFuzzyString(date, now = new Date(), Intl) { function toFuzzyString(date, now, Intl) {
if (!date) { if (!date) {
return ''; return '';
} }
return getBreakpoint(date, now).format(new Date(date), now, Intl); return getBreakpoint(date, now).format(date, now, Intl);
} }
module.exports = { module.exports = {
clearFormatters: clearFormatters,
decayingInterval: decayingInterval, decayingInterval: decayingInterval,
nextFuzzyUpdate: nextFuzzyUpdate, nextFuzzyUpdate: nextFuzzyUpdate,
toFuzzyString: toFuzzyString, toFuzzyString: toFuzzyString,
......
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