From f1532c8aa74943d862fd4e0ca03ddee17c1dd933 Mon Sep 17 00:00:00 2001 From: hudson-newey Date: Tue, 19 Dec 2023 11:41:45 +1000 Subject: [PATCH] Polishing changes --- .../datetime/abstract-datetime.component.ts | 1 + .../datetime/datetime.component.spec.ts | 38 +++++++++------ .../datetime/datetime/datetime.component.ts | 4 +- .../duration/duration.component.spec.ts | 17 +++++-- .../time-since/time-since.component.spec.ts | 47 ++++++++++++------- .../time-since/time-since.component.ts | 13 +++-- .../zoned-datetime.component.spec.ts | 38 ++++++++++++++- 7 files changed, 114 insertions(+), 44 deletions(-) diff --git a/src/app/components/shared/datetime/abstract-datetime.component.ts b/src/app/components/shared/datetime/abstract-datetime.component.ts index 7040f71cc..2b3489a49 100644 --- a/src/app/components/shared/datetime/abstract-datetime.component.ts +++ b/src/app/components/shared/datetime/abstract-datetime.component.ts @@ -1,6 +1,7 @@ import { OnChanges, Component } from "@angular/core"; import { NgbTooltipModule } from "@ng-bootstrap/ng-bootstrap"; +// use use a standalone component here so that it implements all its @Component({ selector: "baw-abstract-datetime", templateUrl: "./abstract-datetime.component.html", diff --git a/src/app/components/shared/datetime/datetime/datetime.component.spec.ts b/src/app/components/shared/datetime/datetime/datetime.component.spec.ts index beacbfc83..da4806aeb 100644 --- a/src/app/components/shared/datetime/datetime/datetime.component.spec.ts +++ b/src/app/components/shared/datetime/datetime/datetime.component.spec.ts @@ -36,6 +36,8 @@ const testCases: TestCase[] = [ name: "should display the full date and time in the users local timezone by default", // create a Luxon DateTime object in UTC+00:00 // we should see this date be localized to UTC+08:00 (the test runners timezone) + // we do not call setZone() here like the other tests because we want to create the DateTime object in UTC+00:00 + // so that we can see it localized to UTC+08:00 value: DateTime.fromISO("2020-01-01T12:10:11.000Z"), expectedText: "2020-01-01 20:10:11", expectedTooltip: "2020-01-01 20:10:11.000 Australia/Perth +08:00", @@ -59,7 +61,10 @@ const testCases: TestCase[] = [ // most of the tests create the DateTime objects in UTC+00:00 // for this test, we create th DateTime object in UTC+02:00 // we should therefore see the hours increase by 6 - name: "should display the correct dateTime if the Luxon dateTime object is a different timezone to the users local timezone", + name: "should display the correct dateTime if the Luxon dateTime object is a different utc offset to the users local timezone", + // by using +02:00, we know the offset, but not the timezone + // however, because this value will be localized to the users local timezone, we should see the timezone emitted + // in the users local timezone value: DateTime.fromISO("2020-01-01T12:10:11.000+02:00"), expectedText: "2020-01-01 18:10:11", // notice that the tooltips are still in the test runners mock timezone @@ -67,6 +72,19 @@ const testCases: TestCase[] = [ expectedTooltip: "2020-01-01 18:10:11.000 Australia/Perth +08:00", expectedDateTimeAttribute: "2020-01-01T18:10:11.000+08:00", }, + { + // this test is the same as the previous offset test, except that we know both the timezone and offset + // in this test. (as opposed to the previous test where we only knew the offset) + name: "should display the correct DateTime if the Luxon dateTime object is a different timezone to the users local timezone", + // by using Australia/Sydney, we know the timezone and offset + value: DateTime.fromISO("2020-01-01T12:10:11.000Z").setZone( + "Australia/Sydney", + { keepLocalTime: true } + ), + expectedText: "2020-01-01 09:10:11", + expectedTooltip: "2020-01-01 09:10:11.000 Australia/Perth +08:00", + expectedDateTimeAttribute: "2020-01-01T09:10:11.000+08:00", + }, { name: "should increment date if timezone localization causes the date to increment", value: DateTime.fromISO("2020-01-01T23:10:11.000Z"), @@ -99,13 +117,6 @@ const testCases: TestCase[] = [ timeOnly: true, expectedDateTimeAttribute: "2020-01-01T20:10:11.000+08:00", }, - { - name: "should have a tooltip that displays the full un-localized utc date, utc time, and utc offset for a Luxon DateTime object", - value: DateTime.fromISO("2020-01-01T12:10:11.000Z"), - expectedText: "2020-01-01 20:10:11", - expectedTooltip: "2020-01-01 20:10:11.000 Australia/Perth +08:00", - expectedDateTimeAttribute: "2020-01-01T20:10:11.000+08:00", - }, { name: "should localize a Luxon DateTime object with a timezone offset", value: DateTime.fromISO("2020-01-01T12:10:11.000+01:00"), @@ -134,17 +145,11 @@ const testCases: TestCase[] = [ expectedTooltip: "2020-01-01 19:10:11.000 Australia/Perth +08:00", expectedDateTimeAttribute: "2020-01-01T19:10:11.000+08:00", }, - { - name: "should have the correct tooltip for a Luxon DateTime object with an offset, but no timezone", - value: new Date("2020-01-01T12:10:11.000+01:00"), - expectedText: "2020-01-01 19:10:11", - expectedTooltip: "2020-01-01 19:10:11.000 Australia/Perth +08:00", - expectedDateTimeAttribute: "2020-01-01T19:10:11.000+08:00", - }, ]; describe("DatetimeComponent", () => { let spectator: Spectator; + let testRunnersDefaultTimezone; const createComponent = createComponentFactory({ component: DatetimeComponent, @@ -171,6 +176,9 @@ describe("DatetimeComponent", () => { beforeEach(() => setup()); + beforeAll(() => testRunnersDefaultTimezone = Settings.defaultZone); + afterAll(() => Settings.defaultZone = testRunnersDefaultTimezone); + it("should create", () => { const fakeDateTime = DateTime.fromISO("2020-01-01T12:10:11.000Z"); spectator.component.value = fakeDateTime; diff --git a/src/app/components/shared/datetime/datetime/datetime.component.ts b/src/app/components/shared/datetime/datetime/datetime.component.ts index ad636da01..c7d7eba94 100644 --- a/src/app/components/shared/datetime/datetime/datetime.component.ts +++ b/src/app/components/shared/datetime/datetime/datetime.component.ts @@ -39,7 +39,9 @@ export class DatetimeComponent extends AbstractDatetimeComponent { } public update(): void { - const fullDateTime = this.luxonDateTime.toFormat("yyyy-MM-dd HH:mm:ss"); + // the fullDateTime is used regardless of format as it is used in the tooltip + // it emits as much information as possible + const fullDateTime = this.luxonDateTime.toFormat("yyyy-MM-dd HH:mm:ss.SSS"); const timezoneName = this.longTimezone(); // we do not place the timezoneName in brackets as the ngx-bootstrap tooltip diff --git a/src/app/components/shared/datetime/duration/duration.component.spec.ts b/src/app/components/shared/datetime/duration/duration.component.spec.ts index a32da7438..a5b407d1e 100644 --- a/src/app/components/shared/datetime/duration/duration.component.spec.ts +++ b/src/app/components/shared/datetime/duration/duration.component.spec.ts @@ -64,11 +64,12 @@ const testCases: TestCase[] = [ expectedTooltip: "00:10:01.500 (PT10M1.5S)", expectedDateTimeAttribute: "PT10M1.5S", humanized: true, - } + }, ]; describe("DurationComponent", () => { let spectator: Spectator; + let originalDefaultZone; const createComponent = createComponentFactory({ component: DurationComponent, @@ -93,13 +94,17 @@ describe("DurationComponent", () => { beforeEach(() => setup()); + beforeAll(() => (originalDefaultZone = Settings.defaultZone)); + afterAll(() => (Settings.defaultZone = originalDefaultZone)); + it("should create", () => { spectator.component.value = modelData.time(); expect(spectator.component).toBeInstanceOf(DurationComponent); }); it("should throw a console error if both iso8601 and humanized props flags are set", () => { - const expectedErrorMessage = "baw-duration: cannot use both iso8601 and humanized"; + const expectedErrorMessage = + "baw-duration: cannot use both iso8601 and humanized"; console.error = jasmine.createSpy("error"); spectator.component.value = modelData.time(); @@ -128,7 +133,9 @@ describe("DurationComponent", () => { }); it("should have the correct text", () => { - expect(componentElement()).toHaveExactTrimmedText(testCase.expectedText); + expect(componentElement()).toHaveExactTrimmedText( + testCase.expectedText + ); }); it("should have the correct tooltip", () => { @@ -136,7 +143,9 @@ describe("DurationComponent", () => { }); it("should have the correct dateTime attribute", () => { - expect(componentElement().dateTime).toBe(testCase.expectedDateTimeAttribute); + expect(componentElement().dateTime).toBe( + testCase.expectedDateTimeAttribute + ); }); }); }); diff --git a/src/app/components/shared/datetime/time-since/time-since.component.spec.ts b/src/app/components/shared/datetime/time-since/time-since.component.spec.ts index cd282ba24..20bce3fb3 100644 --- a/src/app/components/shared/datetime/time-since/time-since.component.spec.ts +++ b/src/app/components/shared/datetime/time-since/time-since.component.spec.ts @@ -16,8 +16,6 @@ interface TestCase { expectedDateTimeAttribute: string; } -// TODO: I recently changed the tooltips for this component and therefore I haven't created the -// test functionality to assert the tooltips const testCases: TestCase[] = [ { name: "should format a duration correctly into a relative time", @@ -26,7 +24,7 @@ const testCases: TestCase[] = [ // the test runners fake date/time is midnight at 2020-01-01 // therefore, by subtracting 2 hours and 10 minutes from that date/time // we get 2019-12-31 21:50:00 (the expected tooltip) - expectedTooltip: "2019-12-31 21:50:00 +00:00", + expectedTooltip: "2019-12-31 21:50:00.000 +00:00", expectedDateTimeAttribute: "PT2H10M", }, { @@ -39,42 +37,52 @@ const testCases: TestCase[] = [ }), expectedText: "4 hours 42 minutes ago", // both the tooltip and the machine readable dateTime attribute should include seconds and milliseconds - expectedTooltip: "2019-12-31 19:17:49 +00:00", + expectedTooltip: "2019-12-31 19:17:49.500 +00:00", expectedDateTimeAttribute: "PT4H42M10.5S", }, { name: "should handle negative durations correctly", value: Duration.fromObject({ hours: -2, minutes: -10 }), expectedText: "2 hours 10 minutes from now", - expectedTooltip: "2020-01-01 02:10:00 +00:00", + expectedTooltip: "2020-01-01 02:10:00.000 +00:00", expectedDateTimeAttribute: "PT-2H-10M", }, - { - name: "should be take a JavaScript Date object and emit how long ago it was in a human readable format", - value: new Date("2019-11-13T14:17:58.000Z"), - expectedText: "1 month 3 weeks ago", - // because all JavaScript dates only support utc and local timezones, we create this test JavaScript Date object in utc - expectedTooltip: "2019-11-13 14:17:58 +00:00", - expectedDateTimeAttribute: "P1M2W6DT9H42M2S", - }, { name: "should be take a Luxon DateTime object and emit how long ago it was in a human readable format", value: DateTime.fromISO("2019-11-13T14:17:58.000Z", { zone: "utc" }), expectedText: "1 month 3 weeks ago", - expectedTooltip: "2019-11-13 14:17:58 +00:00", + expectedTooltip: "2019-11-13 14:17:58.000 +00:00", expectedDateTimeAttribute: "P1M2W6DT9H42M2S", }, { name: "should localize a Luxon dateTime object to the users timezone", value: DateTime.fromISO("2019-12-31T04:17:58.000Z", { zone: "Australia/Darwin" }), expectedText: "19 hours 42 minutes ago", - expectedTooltip: "2019-12-31 13:47:58 Australia/Darwin +09:30", + expectedTooltip: "2019-12-31 13:47:58.000 Australia/Darwin +09:30", expectedDateTimeAttribute: "PT19H42M2S", }, + { + name: "should be take a JavaScript Date object and emit how long ago it was in a human readable format", + value: new Date("2019-11-13T14:17:58.000Z"), + expectedText: "1 month 3 weeks ago", + // because all JavaScript dates only support utc and local timezones, we create this test JavaScript Date object in utc + expectedTooltip: "2019-11-13 14:17:58.000 +00:00", + expectedDateTimeAttribute: "P1M2W6DT9H42M2S", + }, + { + name: "should take a JavaScript Date object with miliseconds and emit how long ago it was in a human readable format", + value: new Date("2019-11-13T14:17:58.500Z"), + expectedText: "1 month 3 weeks ago", + expectedTooltip: "2019-11-13 14:17:58.500 +00:00", + expectedDateTimeAttribute: "P1M2W6DT9H42M1.5S", + } ]; describe("RelativeTimeComponent", () => { let spectator: Spectator; + // because we monkey patch this function for testing, we need to revert the + // changes after the test is run so that it doesn't affect other future tests + let originalDateTimeNow: () => DateTime; const createComponent = createComponentFactory({ component: RelativeTimeComponent, @@ -82,8 +90,6 @@ describe("RelativeTimeComponent", () => { }); function setup(): void { - // because some tests assert the difference between a date and "now", we need to mock "now" - // so that the tests are consistent and don't fail because of a difference of a few milliseconds due to execution time Settings.defaultZone = "utc"; DateTime.now = jasmine .createSpy("now") @@ -103,6 +109,11 @@ describe("RelativeTimeComponent", () => { beforeEach(() => setup()); + // because some tests assert the difference between a date and "now", we need to mock "now" + // so that the tests are consistent and don't fail because of a difference of a few milliseconds due to execution time + beforeAll(() => originalDateTimeNow = DateTime.now); + afterAll(() => DateTime.now = originalDateTimeNow); + it("should create", () => { spectator.component.value = modelData.time(); expect(spectator.component).toBeInstanceOf(RelativeTimeComponent); @@ -112,7 +123,7 @@ describe("RelativeTimeComponent", () => { Settings.defaultZone = "Australia/Perth"; const fakeDuration = Duration.fromObject({ hours: 2, minutes: 10 }); - const expectedTooltip = "2019-12-31 21:50:00 Australia/Perth +08:00"; + const expectedTooltip = "2019-12-31 21:50:00.000 Australia/Perth +08:00"; spectator.component.value = fakeDuration; update(); diff --git a/src/app/components/shared/datetime/time-since/time-since.component.ts b/src/app/components/shared/datetime/time-since/time-since.component.ts index fc81e9145..f9b8b2f8e 100644 --- a/src/app/components/shared/datetime/time-since/time-since.component.ts +++ b/src/app/components/shared/datetime/time-since/time-since.component.ts @@ -38,18 +38,23 @@ export class RelativeTimeComponent extends AbstractDatetimeComponent { return durationDifference.rescale(); } + // because this component can accept a Luxon DateTime, JavaScript Date, or Luxon Duration object + // we want to standardize the shape that we use throughout this component + // therefore, we convert all the formats to a Luxon DateTime object private get luxonStartDate(): DateTime { if (this.value instanceof DateTime) { return this.value; + } else if (this.value instanceof Date) { + return DateTime.fromJSDate(this.value); } - return this.value instanceof Date - ? DateTime.fromJSDate(this.value) - : DateTime.now().minus(this.durationValue); + return DateTime.now().minus(this.durationValue); } public update(): void { - const formattedStartDate = this.luxonStartDate.toFormat("yyyy-MM-dd HH:mm:ss"); + // we include milliseconds in the tooltip, so that we can display as much + // information as possible in the tooltip + const formattedStartDate = this.luxonStartDate.toFormat("yyyy-MM-dd HH:mm:ss.SSS"); const timezoneInformation = this.longTimezone(); this.tooltipValue = `${formattedStartDate} ${timezoneInformation}`; diff --git a/src/app/components/shared/datetime/zoned-datetime/zoned-datetime.component.spec.ts b/src/app/components/shared/datetime/zoned-datetime/zoned-datetime.component.spec.ts index 17661516f..26cde6535 100644 --- a/src/app/components/shared/datetime/zoned-datetime/zoned-datetime.component.spec.ts +++ b/src/app/components/shared/datetime/zoned-datetime/zoned-datetime.component.spec.ts @@ -47,7 +47,7 @@ const testCases: TestCase[] = [ { // in this test, the explicit and implicit timezones are the same // therefore, we should never see any localization occur - name: "should not localize the date/time", + name: "should not localize the date/time that is in the test runners timezone", value: DateTime.fromISO("2020-01-01T12:10:11.000Z").setZone( "Australia/Darwin", // UTC+09:30 { keepLocalTime: true } @@ -101,7 +101,7 @@ const testCases: TestCase[] = [ timeOnly: true, }, { - name: "should use the correct tooltip when we know the offset, but not timezone", + name: "should use the correct information when we know the explicit offset, but not timezone", // we should see that the implicit tooltip has timezone information as we can // derive it from the implicit zone information. // however, when we provide an offset to the explicit timezone, we should see that the tooltip @@ -161,6 +161,21 @@ const testCases: TestCase[] = [ expectedExplicitTooltip: "2020-01-01 12:10:11.123 Australia/Darwin +09:30", expectedExplicitDateTimeAttribute: "2020-01-01T12:10:11.123+09:30", }, + { + name: "should handle fractional JavaScript datetime's correctly", + value: new Date("2020-01-01T12:10:11.123Z"), + explicitTimezone: "Australia/Darwin", + + // the dom text should not include the fractional seconds + // however, the tooltip should include the fractional seconds + expectedImplicitText: "2020-01-01 12:10:11", + expectedImplicitTooltip: "2020-01-01 12:10:11.123 +00:00", + expectedImplicitDateTimeAttribute: "2020-01-01T12:10:11.123Z", + + expectedExplicitText: "2020-01-01 21:40:11", + expectedExplicitTooltip: "2020-01-01 21:40:11.123 Australia/Darwin +09:30", + expectedExplicitDateTimeAttribute: "2020-01-01T21:40:11.123+09:30", + }, // the JavaScript date is created in utc time // therefore, we don't expect any timezone information on the tooltip // but when the timezone is overridden with an explicit timezone @@ -208,6 +223,21 @@ const testCases: TestCase[] = [ timeOnly: true, }, + { + name: "should localize an implicit JavaScript timezone to the explicit timezone", + // because we create the JavaScript date in utc time, we expect the explicit time to + // increase by 9:30 hours (the difference between utc and Australia/Darwin) + value: new Date("2020-01-01T12:10:11.000Z"), + explicitTimezone: "Australia/Darwin", + + expectedImplicitText: "2020-01-01 12:10:11", + expectedImplicitTooltip: "2020-01-01 12:10:11.000 +00:00", + expectedImplicitDateTimeAttribute: "2020-01-01T12:10:11.000Z", + + expectedExplicitText: "2020-01-01 21:40:11", + expectedExplicitTooltip: "2020-01-01 21:40:11.000 Australia/Darwin +09:30", + expectedExplicitDateTimeAttribute: "2020-01-01T21:40:11.000+09:30", + }, ]; /* @@ -225,6 +255,7 @@ const testCases: TestCase[] = [ */ describe("ZonedDateTimeComponent", () => { let spectator: Spectator; + let originalDefaultZone; const createComponent = createComponentFactory({ component: ZonedDateTimeComponent, @@ -246,6 +277,9 @@ describe("ZonedDateTimeComponent", () => { beforeEach(() => setup()); + beforeAll(() => originalDefaultZone = Settings.defaultZone); + afterAll(() => Settings.defaultZone = originalDefaultZone); + it("should create", () => { spectator.component.value = modelData.dateTime(); update();