-
Notifications
You must be signed in to change notification settings - Fork 203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Convert timestamp component to React #1088
Conversation
A notable change is that the current time used to generate the relative timestamp is now stored as an explicit piece of state, rather than implicitly using the current time when calling `toFuzzyString`. This provides a clean way to trigger a re-render of timestamp periodically: an effect updates the "now" time, and that triggers a recalculation of the timestamp. The tests have also been revised to better capture the user-facing behavior of the component.
Codecov Report
@@ Coverage Diff @@
## master #1088 +/- ##
==========================================
+ Coverage 92.51% 92.52% +0.01%
==========================================
Files 164 164
Lines 6343 6340 -3
Branches 1028 1028
==========================================
- Hits 5868 5866 -2
+ Misses 475 474 -1
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Robert, functionally this looks good. I had some thoughts on refactoring/improving the readability of the tests.
src/sidebar/util/time.js
Outdated
* @return {string} A 'fuzzy' string describing the relative age of the date. | ||
*/ | ||
function toFuzzyString(date, Intl) { | ||
function toFuzzyString(date, now = new Date(), Intl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's weird to me that this works but I guess that's a difference between js and python. In python this wouldn't create a new Date every time but looks like that's not the case in js.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is Intl
here-shouldn't that be documented as a param?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intl
here is the browser's Intl
global object which is a namespace for internationalization objects. Yes it should be documented :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's weird to me that this works but I guess that's a difference between js and python.
Yes indeed. In JS the default value is evaluated on every call if the argument is not assigned. I think this is much more useful than Python 😉
src/sidebar/util/test/time-test.js
Outdated
@@ -109,7 +109,7 @@ describe('sidebar.util.time', function() { | |||
const d = new Date().toISOString(); | |||
sandbox.clock.tick(year * 2 * 1000); | |||
|
|||
assert.equal(time.toFuzzyString(d, null), 'Thu Jan 01 1970'); | |||
assert.equal(time.toFuzzyString(d, undefined, null), 'Thu Jan 01 1970'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we should be taking advantage of being able to pass the date to compare against as an argument in these tests now. It would make these tests a lot more explicit and easier to understand.
src/sidebar/util/test/time-test.js
Outdated
}); | ||
|
||
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, mockIntl()), expect); | ||
assert.equal(time.toFuzzyString(t, undefined, mockIntl()), expect); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this really hard to follow and have a couple thoughts about how to fix that. Moving the test cases to the location where they are used would be helpful here in terms of readability, also passing the params explicitly by name here instead of using f[0], f[1]. If we pass in the time to the toFuzzyString rather than undefined we won't have to tick the clock and this test will be much easier to read IMO.
@hmstepanek volunteered to address the issues she raised during code review, so I've assigned to her. |
Hi @hmstepanek - Can you help me get this merged? The tests that you commented that you want to refactor are only lightly touched by this PR for the purposes of being able to test the timestamp component. If you have time to look at them then great, otherwise can that be addressed separately? |
Ah yes let me do this now! |
src/sidebar/util/test/time-test.js
Outdated
} | ||
fakeIntl = sinon.stub().returns({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intl
is a namespace rather than a function. In the browser you access it via Intl.DateTimeFormat
rather than say Intl().DateTimeFormat
or new Intl().DateTimeFormat
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't leave "Request changes" or "Approval" feedback on this PR because it is an extension of my work in the first place. I've left a few notes on the last commit though.
src/sidebar/util/test/time-test.js
Outdated
it('creates correct fuzzy string for fixture ' + test.time, () => { | ||
const epoch = fakeDate('1970-01-01T00:00:00.000Z'); | ||
const epochPlusTimeStamp = new Date(test.time); | ||
assert.equal(time.toFuzzyString(epoch, epochPlusTimeStamp), test.text); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found the variable names confusing here. What is important is not that the first argument happens to be the epoch and the second value happens to be shortly afterwards, but that the first argument is the timestamp
you want to express as a relative date and the second argument is the "current' time.
src/sidebar/util/test/time-test.js
Outdated
} | ||
[ | ||
{ | ||
time: '1970-01-02T03:00:00.000Z', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think time
is ambiguous here. Does it mean the current time or the timestamp you want to format? currentTime
or now
might be better.
@@ -140,9 +146,6 @@ const BREAKPOINTS = [ | |||
]; | |||
|
|||
function getBreakpoint(date, now) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add JSDoc comments to getBreakpoint
to clarify the expected types.
@@ -140,9 +146,6 @@ const BREAKPOINTS = [ | |||
]; | |||
|
|||
function getBreakpoint(date, now) { | |||
// Turn the given ISO 8601 string into a Date object. | |||
date = new Date(date); | |||
|
|||
let breakpoint; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please update the JSDoc comments here to reflect the expected types after your changes.
src/sidebar/util/test/time-test.js
Outdated
].forEach(test => { | ||
it('creates correct fuzzy string for fixture ' + test.time, () => { | ||
const epoch = fakeDate('1970-01-01T00:00:00.000Z'); | ||
const epochPlusTimeStamp = new Date(test.time); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're using a date with a mock getFullYear
function to represent the timestamp being formatted but a normal Date
to represent the current date - where the output can depend on the current timezone.
src/sidebar/components/timestamp.js
Outdated
|
||
// Fuzzy, relative timestamp (eg. '6 days ago') | ||
const prevTime = timestamp ? new Date(timestamp) : null; | ||
const relativeTimestamp = useMemo(() => toFuzzyString(prevTime, now), [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you move the prevTime
calculation inside the useMemo
callback, that will save a little bit of work because the timestamp will only be parsed into a Date
when it changes, rather than every time the component is rendered. Probably completely immaterial compared to all the other work that is happening, but I thought I'd flag it up as an unintended change.
a745df8
to
dedd3cc
Compare
Ok @robertknight. This is ready for a detailed review now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left some small suggestions. I can't approve this since it was my PR originally. Can I leave it to you to do that and merge if you're happy with it @hmstepanek ?
src/sidebar/util/test/time-test.js
Outdated
}, | ||
].forEach(test => { | ||
it( | ||
'passes correct arguments to format fuzzy string date for fixture ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'passes correct arguments to format fuzzy string date for fixture ' + | |
'passes correct arguments to `Intl.DateTimeFormat.format` for fixture ' + |
|
||
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two lines can be combined into one calledWith
check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How are you thinking of combining these together? I'm afraid I'm not seeing it. I could see just getting rid of the first check all-together except that we might perhaps still want to check to see that the value from the format function was actually returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please ignore this. I misread the two "calledWith" asserts as having the same first argument. I've just realized that is not the case.
|
||
assert.equal(time.toFuzzyString(d, null), 'Thu Jan 01 1970'); | ||
assert.equal(time.toFuzzyString(timeStamp, now, null), 'Thu Jan 01 1970'); | ||
assert.calledOnce(timeStamp.toDateString); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably want to reverse the order of these asserts since the first one will lead to the second one.
|
||
assert.equal(time.toFuzzyString(d, null), 'Thu Jan 01 1970'); | ||
assert.equal(time.toFuzzyString(timeStamp, now, null), 'Thu Jan 01 1970'); | ||
assert.calledOnce(timeStamp.toDateString); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, I think it makes sense to check what happens in the function before the return value, and then the return value
src/sidebar/util/time.js
Outdated
* @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 {Object} Intl - The internationalization. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param {Object} Intl - The internationalization. | |
* @param {Object} Intl - JS internationalization API implementation. |
- 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.
dedd3cc
to
5957b12
Compare
(There is no urgency on this PR, I had it sitting around on my system waiting for #1061 to be merged)
A notable change is that the "current" time used to generate the relative
timestamp is now stored as an explicit piece of state, rather than
implicitly using the current time when calling
toFuzzyString
. Thisprovides a clean way to trigger a re-render of timestamp periodically:
an effect updates the "now" time, and that triggers a recalculation of
the relative timestamp string.
The tests have also been revised to better capture the user-facing
behavior of the component.