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 }) {
const [now, setNow] = useState(new Date());
// Fuzzy, relative timestamp (eg. '6 days ago')
const relativeTimestamp = useMemo(() => toFuzzyString(timestamp, now), [
timestamp,
now,
]);
const relativeTimestamp = useMemo(
() => toFuzzyString(timestamp ? new Date(timestamp) : null, now),
[timestamp, now]
);
// Absolute timestamp (eg. 'Tue 22nd Dec 2015, 16:00')
const absoluteTimestamp = useMemo(() => formatDate(new Date(timestamp)), [
......
......@@ -5,128 +5,168 @@ const time = require('../time');
const minute = 60;
const hour = minute * 60;
const day = hour * 24;
const month = day * 30;
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],
];
const msPerSecond = 1000;
describe('sidebar.util.time', function() {
let sandbox;
let fakeIntl;
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
// Ensure that the current local date is 01/01/1970, as this is assumed by
// test expectations
const offset = new Date().getTimezoneOffset();
if (offset > 0) {
sandbox.clock.tick(offset * 60 * 1000);
}
fakeIntl = {
DateTimeFormat: sinon.stub().returns({
format: sinon.stub(),
}),
};
// Clear the formatters cache so that mocked formatters
// from one test run don't affect the next.
time.clearFormatters();
});
afterEach(function() {
sandbox.restore();
});
describe('.toFuzzyString', function() {
function mockIntl() {
return {
DateTimeFormat: function() {
return {
format: function() {
if (new Date().getYear() === 70) {
return '1 Jan';
} else {
return '1 Jan 1970';
}
},
const fakeDate = isoString => {
// Since methods like Date.getFullYear output the year in
// whatever timezone the node timezone is set to, these
// methods must be mocked/mapped to their UTC equivalents when
// testing such as getUTCFullYear in order to have timezone
// agnostic tests.
// Example:
// An annotation was posted at 2019-01-01T01:00:00 UTC and now the
// current date is a few days later; 2019-01-10.
// - 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() {
const t = null;
const date = null;
const expect = '';
assert.equal(time.toFuzzyString(t, undefined, mockIntl()), expect);
assert.equal(time.toFuzzyString(date, undefined), expect);
});
const testFixture = function(f) {
return function() {
const t = new Date().toISOString();
const expect = f[1];
sandbox.clock.tick(f[0] * 1000);
assert.equal(time.toFuzzyString(t, undefined, mockIntl()), expect);
};
};
[
{ now: '1970-01-01T00:00:10.000Z', text: 'Just now' },
{ now: '1970-01-01T00:00:29.000Z', text: 'Just now' },
{ now: '1970-01-01T00:00:49.000Z', text: '49 secs' },
{ now: '1970-01-01T00:01:05.000Z', text: '1 min' },
{ 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() {
// If window.Intl is not available then the date formatting for dates
// more than one day ago falls back to a simple date string.
const d = new Date().toISOString();
sandbox.clock.tick(day * 2 * 1000);
const timeStamp = fakeDate('1970-01-01T00:00:00.000Z');
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() {
// If window.Intl is not available then the date formatting for dates
// more than one year ago falls back to a simple date string.
const d = new Date().toISOString();
sandbox.clock.tick(year * 2 * 1000);
const timeStamp = fakeDate('1970-01-01T00:00:00.000Z');
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() {
it('Handles empty dates', function() {
const date = null;
time.decayingInterval(date, undefined);
});
it('uses a short delay for recent timestamps', function() {
const date = new Date();
const date = new Date().toISOString();
const callback = sandbox.stub();
time.decayingInterval(date, callback);
sandbox.clock.tick(6 * 1000);
sandbox.clock.tick(6 * msPerSecond);
assert.calledWith(callback, date);
sandbox.clock.tick(6 * 1000);
sandbox.clock.tick(6 * msPerSecond);
assert.calledTwice(callback);
});
it('uses a longer delay for older timestamps', function() {
const date = new Date();
const ONE_MINUTE = minute * 1000;
const date = new Date().toISOString();
const ONE_MINUTE = minute * msPerSecond;
sandbox.clock.tick(10 * ONE_MINUTE);
const callback = sandbox.stub();
time.decayingInterval(date, callback);
......@@ -139,17 +179,17 @@ describe('sidebar.util.time', function() {
});
it('returned function cancels the timer', function() {
const date = new Date();
const date = new Date().toISOString();
const callback = sandbox.stub();
const cancel = time.decayingInterval(date, callback);
cancel();
sandbox.clock.tick(minute * 1000);
sandbox.clock.tick(minute * msPerSecond);
assert.notCalled(callback);
});
it('does not set a timeout for dates > 24hrs ago', function() {
const date = new Date();
const ONE_DAY = day * 1000;
const date = new Date().toISOString();
const ONE_DAY = day * msPerSecond;
sandbox.clock.tick(10 * ONE_DAY);
const callback = sandbox.stub();
......@@ -162,26 +202,31 @@ describe('sidebar.util.time', function() {
describe('.nextFuzzyUpdate', function() {
it('Handles empty dates', function() {
const t = null;
const date = null;
const expect = null;
assert.equal(time.nextFuzzyUpdate(t), expect);
assert.equal(time.nextFuzzyUpdate(date, undefined), expect);
});
const testFixture = function(f) {
return function() {
const t = new Date().toISOString();
const expect = f[1];
sandbox.clock.tick(f[0] * 1000);
assert.equal(time.nextFuzzyUpdate(t), expect);
};
};
for (let i = 0, f; i < FIXTURES_NEXT_FUZZY_UPDATE.length; i++) {
f = FIXTURES_NEXT_FUZZY_UPDATE[i];
it(
'gives correct next fuzzy update time for fixture ' + i,
testFixture(f)
[
{ now: '1970-01-01T00:00:10.000Z', expectedUpdateTime: 5 }, // we have a minimum of 5 secs
{ now: '1970-01-01T00:00:20.000Z', expectedUpdateTime: 5 },
{ now: '1970-01-01T00:00:49.000Z', expectedUpdateTime: 5 },
{ now: '1970-01-01T00:01:05.000Z', expectedUpdateTime: minute },
{ 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 },
{ now: '1970-07-02T00:00:00.000Z', expectedUpdateTime: null },
{ now: '1978-01-01T00:00:00.000Z', expectedUpdateTime: null },
].forEach(test => {
it('gives correct next fuzzy update time for fixture ' + test.now, () => {
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) {
// Cached DateTimeFormat instances,
// because instantiating a DateTimeFormat is expensive.
const formatters = {};
let formatters = {};
/**
* Clears the cache of formatters.
*/
function clearFormatters() {
formatters = {};
}
/**
* Efficiently return `date` formatted with `options`.
*
......@@ -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) {
// Turn the given ISO 8601 string into a Date object.
date = new Date(date);
let breakpoint;
for (let i = 0; i < BREAKPOINTS.length; i++) {
breakpoint = BREAKPOINTS[i];
......@@ -154,12 +165,12 @@ function getBreakpoint(date, now) {
return null;
}
function nextFuzzyUpdate(date) {
function nextFuzzyUpdate(date, now) {
if (!date) {
return null;
}
let secs = getBreakpoint(date, new Date()).nextUpdate;
let secs = getBreakpoint(date, now).nextUpdate;
if (secs === null) {
return null;
......@@ -183,12 +194,15 @@ function nextFuzzyUpdate(date) {
* This can be used to refresh parts of a UI whose
* 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.
*/
function decayingInterval(date, callback) {
let timer;
const timeStamp = date ? new Date(date) : null;
const update = function() {
const fuzzyUpdate = nextFuzzyUpdate(date);
const fuzzyUpdate = nextFuzzyUpdate(timeStamp, new Date());
if (fuzzyUpdate === null) {
return;
}
......@@ -208,18 +222,20 @@ function decayingInterval(date, callback) {
/**
* Formats a date as a string relative to the current date.
*
* @param {number} date - The absolute timestamp to format.
* @param {Date} [now] - The date to consider as the current time.
* @param {Date} date - The date to consider as the timestamp to format.
* @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.
*/
function toFuzzyString(date, now = new Date(), Intl) {
function toFuzzyString(date, now, Intl) {
if (!date) {
return '';
}
return getBreakpoint(date, now).format(new Date(date), now, Intl);
return getBreakpoint(date, now).format(date, now, Intl);
}
module.exports = {
clearFormatters: clearFormatters,
decayingInterval: decayingInterval,
nextFuzzyUpdate: nextFuzzyUpdate,
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