From 07c9bde584e25e307fa697ba6e2e10d051f9bf72 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Thu, 15 Dec 2022 19:14:31 +0900 Subject: [PATCH 1/9] feat: add hook to adjust for server time [LIBS-396] --- runtime/src/index.ts | 2 +- services/config/package.json | 2 +- .../config/src/__tests__/useDate.test.tsx | 131 +++++++++++++++++ services/config/src/index.ts | 1 + services/config/src/types.ts | 13 ++ services/config/src/useDate.ts | 134 ++++++++++++++++++ 6 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 services/config/src/__tests__/useDate.test.tsx create mode 100644 services/config/src/useDate.ts diff --git a/runtime/src/index.ts b/runtime/src/index.ts index a3f580d9..1dd7f4fd 100644 --- a/runtime/src/index.ts +++ b/runtime/src/index.ts @@ -9,7 +9,7 @@ export { useDataEngine, } from '@dhis2/app-service-data' -export { useConfig } from '@dhis2/app-service-config' +export { useConfig, useDate } from '@dhis2/app-service-config' export { useAlerts, useAlert } from '@dhis2/app-service-alerts' diff --git a/services/config/package.json b/services/config/package.json index 505a9be4..ee4b71ad 100644 --- a/services/config/package.json +++ b/services/config/package.json @@ -34,7 +34,7 @@ "watch": "NODE_ENV=development concurrently -n build,types \"yarn build:package --watch\" \"yarn build:types --watch\"", "type-check": "tsc --noEmit --allowJs --checkJs", "type-check:watch": "yarn type-check --watch", - "test": "d2-app-scripts test", + "test": "TZ=Etc/UTC d2-app-scripts test", "coverage": "yarn test --coverage" } } diff --git a/services/config/src/__tests__/useDate.test.tsx b/services/config/src/__tests__/useDate.test.tsx new file mode 100644 index 00000000..68a2771f --- /dev/null +++ b/services/config/src/__tests__/useDate.test.tsx @@ -0,0 +1,131 @@ +import { renderHook } from '@testing-library/react-hooks' +import React, { ReactNode } from 'react' +import { ConfigProvider, useDate } from '../index' + +const defaultConfig = { baseUrl: '/', apiVersion: 40 } +const defaultSystemInfo = { + version: '40', + contextPath: '', + serverTimeZoneId: 'UTC', +} + +// tests are set to run at UTC when running yarn test + +describe('useDate', () => { + it('Hook returns a fromClientDate and fromServerDate function', () => { + const config = { baseUrl: '/', apiVersion: 30 } + const wrapper = ({ children }: { children?: ReactNode }) => ( + {children} + ) + const { result } = renderHook(() => useDate(), { wrapper }) + + expect(result.current).toHaveProperty('fromClientDate') + expect(typeof result.current.fromClientDate).toBe('function') + expect(result.current).toHaveProperty('fromServerDate') + expect(typeof result.current.fromServerDate).toBe('function') + }) + + it('returns fromServerDate that corrects for server time zone', () => { + const systemInfo = { + ...defaultSystemInfo, + serverTimeZoneId: 'Europe/Oslo', + } + const config = { ...defaultConfig, systemInfo } + const wrapper = ({ children }: { children?: ReactNode }) => ( + {children} + ) + const { result } = renderHook(() => useDate(), { wrapper }) + + const serverDate = result.current.fromServerDate('2010-01-01') + const expectedDateString = '2009-12-31T23:00:00.000' + expect(serverDate.getClientISOString()).toBe(expectedDateString) + }) + + // fromServerDate accepts number, valid date string, or date object + it('returns fromServerDate which accepts number, valid date string, or date object', () => { + const config = { ...defaultConfig, systemInfo: defaultSystemInfo } + const wrapper = ({ children }: { children?: ReactNode }) => ( + {children} + ) + const { result } = renderHook(() => useDate(), { wrapper }) + + const dateString = '2010-01-01' + const dateFromString = new Date('2010-01-01') + const millisecondsAfterUTC = dateFromString.getTime() + + const serverDateFromString = result.current.fromServerDate(dateString) + const serverDateFromDate = result.current.fromServerDate(dateFromString) + const serverDateFromNumber = + result.current.fromServerDate(millisecondsAfterUTC) + + expect(serverDateFromString).toEqual(serverDateFromDate) + expect(serverDateFromString).toEqual(serverDateFromNumber) + }) + + // returns current (client) date if no argument is provided + // it('returns fromServerDate which returns an invalid date if ', () => { + // const config = {...defaultConfig,systemInfo: defaultSystemInfo} + // const wrapper = ({ children }: { children?: ReactNode }) => ( + // {children} + // ) + // const { result } = renderHook(() => useDate(), {wrapper}) + + // const now = result.current.fromServerDate() + // const nowDirect = new Date() + + // expect(nowDirect.getTime()-now.getTime()).toBeLessThan(1000) + // }) + + // fromServerDate defaults to client time zone if invalid server time zone provided + it('returns fromServerDate that assumes no time zone difference if provided time zone is invalid', () => { + const systemInfo = { + ...defaultSystemInfo, + serverTimeZoneId: 'Asia/Oslo', + } + const config = { ...defaultConfig, systemInfo } + const wrapper = ({ children }: { children?: ReactNode }) => ( + {children} + ) + const { result } = renderHook(() => useDate(), { wrapper }) + + const serverDate = result.current.fromServerDate('2010-01-01') + const expectedDateString = '2010-01-01T00:00:00.000' + expect(serverDate.getClientISOString()).toBe(expectedDateString) + }) + + it('returns fromServerDate with server date that matches passed time regardless of timezone', () => { + const systemInfo = { + ...defaultSystemInfo, + serverTimeZoneId: 'Asia/Jakarta', + } + const config = { ...defaultConfig, systemInfo } + const wrapper = ({ children }: { children?: ReactNode }) => ( + {children} + ) + const { result } = renderHook(() => useDate(), { wrapper }) + + const serverDate = result.current.fromServerDate('2015-03-03T12:00:00') + const expectedDateString = '2015-03-03T12:00:00.000' + expect(serverDate.getServerISOString()).toBe(expectedDateString) + }) + + it('returns fromClientDate that reflects client time but makes server time string accessible', () => { + const systemInfo = { + ...defaultSystemInfo, + serverTimeZoneId: 'America/Guatemala', + } + const config = { ...defaultConfig, systemInfo } + const wrapper = ({ children }: { children?: ReactNode }) => ( + {children} + ) + const { result } = renderHook(() => useDate(), { wrapper }) + + const serverDate = result.current.fromClientDate('2018-08-15T12:00:00') + const expectedClientDateString = '2018-08-15T12:00:00.000' + const expectedServerDateString = '2018-08-15T06:00:00.000' + const javascriptDate = new Date('2018-08-15T12:00:00') + expect(serverDate.getClientISOString()).toBe(expectedClientDateString) + expect(serverDate.getServerISOString()).toBe(expectedServerDateString) + expect(serverDate.getTime()).toEqual(javascriptDate.getTime()) + }) +}) diff --git a/services/config/src/index.ts b/services/config/src/index.ts index 790d6ba1..908218f8 100644 --- a/services/config/src/index.ts +++ b/services/config/src/index.ts @@ -1,4 +1,5 @@ export { useConfig } from './useConfig' +export { useDate } from './useDate' export { ConfigProvider } from './ConfigProvider' export type { Config } from './types' diff --git a/services/config/src/types.ts b/services/config/src/types.ts index f4fd397d..02d44b74 100644 --- a/services/config/src/types.ts +++ b/services/config/src/types.ts @@ -6,9 +6,22 @@ type Version = { tag?: string } +export type DateInput = string | Date | number | null + +export interface DateComponents { + year: string + month: string + days: string + hours: string + minutes: string + seconds: string + milliseconds: string +} + interface SystemInfo { version: string contextPath: string + serverTimeZoneId: string } export interface Config { diff --git a/services/config/src/useDate.ts b/services/config/src/useDate.ts new file mode 100644 index 00000000..5a88b834 --- /dev/null +++ b/services/config/src/useDate.ts @@ -0,0 +1,134 @@ +import { useCallback, useContext, useMemo } from 'react' +import { ConfigContext } from './ConfigContext' +import { DateComponents, DateInput } from './types' + +// extend date with extra methods +class DHIS2Date extends Date { + serverOffset: number + serverTimezone: string + clientTimezone: string + + constructor({ + date, + serverOffset, + serverTimezone, + clientTimezone, + }: { + date: DateInput + serverOffset: number + serverTimezone: string + clientTimezone: string + }) { + if (date) { + super(date) + } else { + super() + } + this.serverOffset = serverOffset + this.serverTimezone = serverTimezone + this.clientTimezone = clientTimezone + Object.setPrototypeOf(this, DHIS2Date.prototype) + } + + private _getTimeComponents(date: Date): DateComponents { + const year = date.getFullYear().toString().padStart(4, '0') + const month = (date.getMonth() + 1).toString().padStart(2, '0') + const days = date.getDate().toString().padStart(2, '0') + const hours = date.getHours().toString().padStart(2, '0') + const minutes = date.getMinutes().toString().padStart(2, '0') + const seconds = date.getSeconds().toString().padStart(2, '0') + const milliseconds = date.getMilliseconds().toString().padStart(3, '0') + return { year, month, days, hours, minutes, seconds, milliseconds } + } + + private _getISOString(date: Date): string { + const { year, month, days, hours, minutes, seconds, milliseconds } = + this._getTimeComponents(date) + return `${year}-${month}-${days}T${hours}:${minutes}:${seconds}.${milliseconds}` + } + + public getServerISOString(): string { + const serverDate = new Date(this.getTime() - this.serverOffset) + return this._getISOString(serverDate) + } + + public getClientISOString(): string { + return this._getISOString(this) + } +} + +const useServerTimeOffset = (serverTimezone: string): number => { + return useMemo(() => { + try { + const nowClientTime = new Date() + nowClientTime.setMilliseconds(0) + + // 'sv' is used for localeString because it is the closest to ISO format + // in principle, any locale should be parsable back to a date, but we encountered an error + // when using en-US in certain environments, which we could not replicate when using 'sv' + // Converting to localeString and then back to date is unfortunately the only current way + // to construct a date that accounts for timezone. + const serverLocaleString = nowClientTime.toLocaleString('sv', { + timeZone: serverTimezone, + }) + const nowServerTimeZone = new Date(serverLocaleString) + nowServerTimeZone.setMilliseconds(0) + + return nowClientTime.getTime() - nowServerTimeZone.getTime() + } catch (err) { + console.error( + 'Server time offset could not be determined; assuming no client/server difference', + err + ) + // if date is not constructable with timezone, assume 0 difference between client/server + return 0 + } + }, [serverTimezone]) +} + +export const useDate = (): { + fromServerDate: (date?: DateInput) => DHIS2Date + fromClientDate: (date?: DateInput) => DHIS2Date +} => { + const { systemInfo } = useContext(ConfigContext) + const clientTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone + const { serverTimeZoneId: serverTimezone } = systemInfo || { + serverTimeZoneId: clientTimezone, + } + + const serverOffset = useServerTimeOffset(serverTimezone) + + const fromServerDate = useCallback( + (date) => { + const serverDate = new Date(date) + const clientDate = new DHIS2Date({ + date: serverDate.getTime() + serverOffset, + serverOffset, + serverTimezone, + clientTimezone, + }) + + return clientDate + }, + [serverOffset, serverTimezone, clientTimezone] + ) + + const fromClientDate = useCallback( + (date) => { + const clientDate = new DHIS2Date({ + date, + serverOffset, + serverTimezone, + clientTimezone, + }) + + return clientDate + }, + [serverOffset, serverTimezone, clientTimezone] + ) + + return useMemo( + () => ({ fromServerDate, fromClientDate }), + [fromServerDate, fromClientDate] + ) +} From ec0257c59444a4354e7feaaf82d2578e1c60f45e Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Wed, 11 Jan 2023 10:35:24 +0100 Subject: [PATCH 2/9] fix: update provide warning when serverTimeZoneId is unavailable Co-authored-by: Hendrik de Graaf --- services/config/src/useDate.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/services/config/src/useDate.ts b/services/config/src/useDate.ts index 5a88b834..fe479c80 100644 --- a/services/config/src/useDate.ts +++ b/services/config/src/useDate.ts @@ -90,10 +90,15 @@ export const useDate = (): { fromServerDate: (date?: DateInput) => DHIS2Date fromClientDate: (date?: DateInput) => DHIS2Date } => { - const { systemInfo } = useContext(ConfigContext) - const clientTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone - const { serverTimeZoneId: serverTimezone } = systemInfo || { - serverTimeZoneId: clientTimezone, + const { systemInfo } = useConfig() + let serverTimezone + + if (systemInfo?.serverTimeZoneId) { + serverTimezone = systemInfo.serverTimeZoneId + } else { + // Fallback to client timezone + serverTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone + console.warn('No server timezone ID found, falling back to client timezone. This could cause date conversion issues.') } const serverOffset = useServerTimeOffset(serverTimezone) From 0ffc5daf1d0788ee49dd20e44fbb94ed7f53d719 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Wed, 11 Jan 2023 10:45:06 +0100 Subject: [PATCH 3/9] fix: add in useConfig import and clean up --- services/config/src/useDate.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/config/src/useDate.ts b/services/config/src/useDate.ts index fe479c80..246419e3 100644 --- a/services/config/src/useDate.ts +++ b/services/config/src/useDate.ts @@ -1,6 +1,7 @@ -import { useCallback, useContext, useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { ConfigContext } from './ConfigContext' import { DateComponents, DateInput } from './types' +import { useConfig } from './useConfig' // extend date with extra methods class DHIS2Date extends Date { @@ -91,13 +92,14 @@ export const useDate = (): { fromClientDate: (date?: DateInput) => DHIS2Date } => { const { systemInfo } = useConfig() - let serverTimezone + let serverTimezone: string + const clientTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone if (systemInfo?.serverTimeZoneId) { serverTimezone = systemInfo.serverTimeZoneId } else { // Fallback to client timezone - serverTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone + serverTimezone = clientTimezone console.warn('No server timezone ID found, falling back to client timezone. This could cause date conversion issues.') } From e8a8d8eb44f411b97f4003ae85e548af7c379b99 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Wed, 11 Jan 2023 11:02:12 +0100 Subject: [PATCH 4/9] fix: update hook/function names --- runtime/src/index.ts | 2 +- ...est.tsx => useTimeZoneConversion.test.tsx} | 28 +++++++++---------- services/config/src/index.ts | 2 +- .../{useDate.ts => useTimeZoneConversion.ts} | 6 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) rename services/config/src/__tests__/{useDate.test.tsx => useTimeZoneConversion.test.tsx} (82%) rename services/config/src/{useDate.ts => useTimeZoneConversion.ts} (97%) diff --git a/runtime/src/index.ts b/runtime/src/index.ts index 1dd7f4fd..ae1185e2 100644 --- a/runtime/src/index.ts +++ b/runtime/src/index.ts @@ -9,7 +9,7 @@ export { useDataEngine, } from '@dhis2/app-service-data' -export { useConfig, useDate } from '@dhis2/app-service-config' +export { useConfig, useTimeZoneConversion } from '@dhis2/app-service-config' export { useAlerts, useAlert } from '@dhis2/app-service-alerts' diff --git a/services/config/src/__tests__/useDate.test.tsx b/services/config/src/__tests__/useTimeZoneConversion.test.tsx similarity index 82% rename from services/config/src/__tests__/useDate.test.tsx rename to services/config/src/__tests__/useTimeZoneConversion.test.tsx index 68a2771f..7f744389 100644 --- a/services/config/src/__tests__/useDate.test.tsx +++ b/services/config/src/__tests__/useTimeZoneConversion.test.tsx @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react-hooks' import React, { ReactNode } from 'react' -import { ConfigProvider, useDate } from '../index' +import { ConfigProvider, useTimeZoneConversion } from '../index' const defaultConfig = { baseUrl: '/', apiVersion: 40 } const defaultSystemInfo = { @@ -11,13 +11,13 @@ const defaultSystemInfo = { // tests are set to run at UTC when running yarn test -describe('useDate', () => { +describe('useTimeZoneConversion', () => { it('Hook returns a fromClientDate and fromServerDate function', () => { const config = { baseUrl: '/', apiVersion: 30 } const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useDate(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) expect(result.current).toHaveProperty('fromClientDate') expect(typeof result.current.fromClientDate).toBe('function') @@ -34,11 +34,11 @@ describe('useDate', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useDate(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) const serverDate = result.current.fromServerDate('2010-01-01') const expectedDateString = '2009-12-31T23:00:00.000' - expect(serverDate.getClientISOString()).toBe(expectedDateString) + expect(serverDate.getClientZonedISOString()).toBe(expectedDateString) }) // fromServerDate accepts number, valid date string, or date object @@ -47,7 +47,7 @@ describe('useDate', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useDate(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) const dateString = '2010-01-01' const dateFromString = new Date('2010-01-01') @@ -68,7 +68,7 @@ describe('useDate', () => { // const wrapper = ({ children }: { children?: ReactNode }) => ( // {children} // ) - // const { result } = renderHook(() => useDate(), {wrapper}) + // const { result } = renderHook(() => useTimeZoneConversion(), {wrapper}) // const now = result.current.fromServerDate() // const nowDirect = new Date() @@ -86,11 +86,11 @@ describe('useDate', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useDate(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) const serverDate = result.current.fromServerDate('2010-01-01') const expectedDateString = '2010-01-01T00:00:00.000' - expect(serverDate.getClientISOString()).toBe(expectedDateString) + expect(serverDate.getClientZonedISOString()).toBe(expectedDateString) }) it('returns fromServerDate with server date that matches passed time regardless of timezone', () => { @@ -102,11 +102,11 @@ describe('useDate', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useDate(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) const serverDate = result.current.fromServerDate('2015-03-03T12:00:00') const expectedDateString = '2015-03-03T12:00:00.000' - expect(serverDate.getServerISOString()).toBe(expectedDateString) + expect(serverDate.getServerZonedISOString()).toBe(expectedDateString) }) it('returns fromClientDate that reflects client time but makes server time string accessible', () => { @@ -118,14 +118,14 @@ describe('useDate', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useDate(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) const serverDate = result.current.fromClientDate('2018-08-15T12:00:00') const expectedClientDateString = '2018-08-15T12:00:00.000' const expectedServerDateString = '2018-08-15T06:00:00.000' const javascriptDate = new Date('2018-08-15T12:00:00') - expect(serverDate.getClientISOString()).toBe(expectedClientDateString) - expect(serverDate.getServerISOString()).toBe(expectedServerDateString) + expect(serverDate.getClientZonedISOString()).toBe(expectedClientDateString) + expect(serverDate.getServerZonedISOString()).toBe(expectedServerDateString) expect(serverDate.getTime()).toEqual(javascriptDate.getTime()) }) }) diff --git a/services/config/src/index.ts b/services/config/src/index.ts index 908218f8..dc132ee9 100644 --- a/services/config/src/index.ts +++ b/services/config/src/index.ts @@ -1,5 +1,5 @@ export { useConfig } from './useConfig' -export { useDate } from './useDate' +export { useTimeZoneConversion } from './useTimeZoneConversion' export { ConfigProvider } from './ConfigProvider' export type { Config } from './types' diff --git a/services/config/src/useDate.ts b/services/config/src/useTimeZoneConversion.ts similarity index 97% rename from services/config/src/useDate.ts rename to services/config/src/useTimeZoneConversion.ts index 246419e3..0808f385 100644 --- a/services/config/src/useDate.ts +++ b/services/config/src/useTimeZoneConversion.ts @@ -48,12 +48,12 @@ class DHIS2Date extends Date { return `${year}-${month}-${days}T${hours}:${minutes}:${seconds}.${milliseconds}` } - public getServerISOString(): string { + public getServerZonedISOString(): string { const serverDate = new Date(this.getTime() - this.serverOffset) return this._getISOString(serverDate) } - public getClientISOString(): string { + public getClientZonedISOString(): string { return this._getISOString(this) } } @@ -87,7 +87,7 @@ const useServerTimeOffset = (serverTimezone: string): number => { }, [serverTimezone]) } -export const useDate = (): { +export const useTimeZoneConversion = (): { fromServerDate: (date?: DateInput) => DHIS2Date fromClientDate: (date?: DateInput) => DHIS2Date } => { From 09b5f0512ec0fd0be9769f9cd4a862ecf635678b Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Wed, 11 Jan 2023 11:12:08 +0100 Subject: [PATCH 5/9] fix: linting on useTimeZoneConversion.test --- .../__tests__/useTimeZoneConversion.test.tsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/services/config/src/__tests__/useTimeZoneConversion.test.tsx b/services/config/src/__tests__/useTimeZoneConversion.test.tsx index 7f744389..879bfbb1 100644 --- a/services/config/src/__tests__/useTimeZoneConversion.test.tsx +++ b/services/config/src/__tests__/useTimeZoneConversion.test.tsx @@ -17,7 +17,9 @@ describe('useTimeZoneConversion', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { + wrapper, + }) expect(result.current).toHaveProperty('fromClientDate') expect(typeof result.current.fromClientDate).toBe('function') @@ -34,7 +36,9 @@ describe('useTimeZoneConversion', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { + wrapper, + }) const serverDate = result.current.fromServerDate('2010-01-01') const expectedDateString = '2009-12-31T23:00:00.000' @@ -47,7 +51,9 @@ describe('useTimeZoneConversion', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { + wrapper, + }) const dateString = '2010-01-01' const dateFromString = new Date('2010-01-01') @@ -86,7 +92,9 @@ describe('useTimeZoneConversion', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { + wrapper, + }) const serverDate = result.current.fromServerDate('2010-01-01') const expectedDateString = '2010-01-01T00:00:00.000' @@ -102,7 +110,9 @@ describe('useTimeZoneConversion', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { + wrapper, + }) const serverDate = result.current.fromServerDate('2015-03-03T12:00:00') const expectedDateString = '2015-03-03T12:00:00.000' @@ -118,14 +128,20 @@ describe('useTimeZoneConversion', () => { const wrapper = ({ children }: { children?: ReactNode }) => ( {children} ) - const { result } = renderHook(() => useTimeZoneConversion(), { wrapper }) + const { result } = renderHook(() => useTimeZoneConversion(), { + wrapper, + }) const serverDate = result.current.fromClientDate('2018-08-15T12:00:00') const expectedClientDateString = '2018-08-15T12:00:00.000' const expectedServerDateString = '2018-08-15T06:00:00.000' const javascriptDate = new Date('2018-08-15T12:00:00') - expect(serverDate.getClientZonedISOString()).toBe(expectedClientDateString) - expect(serverDate.getServerZonedISOString()).toBe(expectedServerDateString) + expect(serverDate.getClientZonedISOString()).toBe( + expectedClientDateString + ) + expect(serverDate.getServerZonedISOString()).toBe( + expectedServerDateString + ) expect(serverDate.getTime()).toEqual(javascriptDate.getTime()) }) }) From 7d62d1410c1555821297a1570da88fbd27c2a72d Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Wed, 11 Jan 2023 11:19:25 +0100 Subject: [PATCH 6/9] fix: fix linting for useTimeZoneConversion --- services/config/src/useTimeZoneConversion.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/config/src/useTimeZoneConversion.ts b/services/config/src/useTimeZoneConversion.ts index 0808f385..8e6b3c18 100644 --- a/services/config/src/useTimeZoneConversion.ts +++ b/services/config/src/useTimeZoneConversion.ts @@ -93,14 +93,17 @@ export const useTimeZoneConversion = (): { } => { const { systemInfo } = useConfig() let serverTimezone: string - const clientTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone - + const clientTimezone: string = + Intl.DateTimeFormat().resolvedOptions().timeZone + if (systemInfo?.serverTimeZoneId) { serverTimezone = systemInfo.serverTimeZoneId } else { // Fallback to client timezone serverTimezone = clientTimezone - console.warn('No server timezone ID found, falling back to client timezone. This could cause date conversion issues.') + console.warn( + 'No server timezone ID found, falling back to client timezone. This could cause date conversion issues.' + ) } const serverOffset = useServerTimeOffset(serverTimezone) From df418bcbdebd8dde3bbdf4c9c1966973024d3568 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Tue, 17 Jan 2023 16:14:18 +0100 Subject: [PATCH 7/9] fix: mock Date.now for testing --- .../__tests__/useTimeZoneConversion.test.tsx | 30 +++++++++++-------- services/config/src/useTimeZoneConversion.ts | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/services/config/src/__tests__/useTimeZoneConversion.test.tsx b/services/config/src/__tests__/useTimeZoneConversion.test.tsx index 879bfbb1..0a209aee 100644 --- a/services/config/src/__tests__/useTimeZoneConversion.test.tsx +++ b/services/config/src/__tests__/useTimeZoneConversion.test.tsx @@ -69,18 +69,24 @@ describe('useTimeZoneConversion', () => { }) // returns current (client) date if no argument is provided - // it('returns fromServerDate which returns an invalid date if ', () => { - // const config = {...defaultConfig,systemInfo: defaultSystemInfo} - // const wrapper = ({ children }: { children?: ReactNode }) => ( - // {children} - // ) - // const { result } = renderHook(() => useTimeZoneConversion(), {wrapper}) - - // const now = result.current.fromServerDate() - // const nowDirect = new Date() - - // expect(nowDirect.getTime()-now.getTime()).toBeLessThan(1000) - // }) + it('returns fromServerDate which returns current timestamp if no argument is passed', () => { + const config = { ...defaultConfig, systemInfo: defaultSystemInfo } + const wrapper = ({ children }: { children?: ReactNode }) => ( + {children} + ) + const { result } = renderHook(() => useTimeZoneConversion(), { + wrapper, + }) + + // if no date-like is passed to fromSeverDate, Date.now() is used to initialize date + jest.spyOn(global.Date, 'now').mockImplementation(() => + new Date('2020-10-15T12:00:00.000Z').valueOf() + ) + + const timeFromHook = result.current.fromServerDate() + + expect(timeFromHook).toEqual(new Date('2020-10-15T12:00:00.000Z')) + }) // fromServerDate defaults to client time zone if invalid server time zone provided it('returns fromServerDate that assumes no time zone difference if provided time zone is invalid', () => { diff --git a/services/config/src/useTimeZoneConversion.ts b/services/config/src/useTimeZoneConversion.ts index 8e6b3c18..1d90f285 100644 --- a/services/config/src/useTimeZoneConversion.ts +++ b/services/config/src/useTimeZoneConversion.ts @@ -23,7 +23,7 @@ class DHIS2Date extends Date { if (date) { super(date) } else { - super() + super(Date.now()) } this.serverOffset = serverOffset this.serverTimezone = serverTimezone From 96d42288a853338113203c20eff64dd8d81d8449 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Tue, 17 Jan 2023 16:26:42 +0100 Subject: [PATCH 8/9] fix: clean up from PR comments --- services/config/src/types.ts | 10 ---------- services/config/src/useTimeZoneConversion.ts | 12 ++---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/services/config/src/types.ts b/services/config/src/types.ts index 02d44b74..6391e812 100644 --- a/services/config/src/types.ts +++ b/services/config/src/types.ts @@ -8,16 +8,6 @@ type Version = { export type DateInput = string | Date | number | null -export interface DateComponents { - year: string - month: string - days: string - hours: string - minutes: string - seconds: string - milliseconds: string -} - interface SystemInfo { version: string contextPath: string diff --git a/services/config/src/useTimeZoneConversion.ts b/services/config/src/useTimeZoneConversion.ts index 1d90f285..77aa0978 100644 --- a/services/config/src/useTimeZoneConversion.ts +++ b/services/config/src/useTimeZoneConversion.ts @@ -1,6 +1,5 @@ import { useCallback, useMemo } from 'react' -import { ConfigContext } from './ConfigContext' -import { DateComponents, DateInput } from './types' +import { DateInput } from './types' import { useConfig } from './useConfig' // extend date with extra methods @@ -28,10 +27,9 @@ class DHIS2Date extends Date { this.serverOffset = serverOffset this.serverTimezone = serverTimezone this.clientTimezone = clientTimezone - Object.setPrototypeOf(this, DHIS2Date.prototype) } - private _getTimeComponents(date: Date): DateComponents { + private _getISOString(date: Date): string { const year = date.getFullYear().toString().padStart(4, '0') const month = (date.getMonth() + 1).toString().padStart(2, '0') const days = date.getDate().toString().padStart(2, '0') @@ -39,12 +37,6 @@ class DHIS2Date extends Date { const minutes = date.getMinutes().toString().padStart(2, '0') const seconds = date.getSeconds().toString().padStart(2, '0') const milliseconds = date.getMilliseconds().toString().padStart(3, '0') - return { year, month, days, hours, minutes, seconds, milliseconds } - } - - private _getISOString(date: Date): string { - const { year, month, days, hours, minutes, seconds, milliseconds } = - this._getTimeComponents(date) return `${year}-${month}-${days}T${hours}:${minutes}:${seconds}.${milliseconds}` } From ee9b039c5db4fc51d59ba6f58cc0136b058d4827 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Tue, 17 Jan 2023 16:37:43 +0100 Subject: [PATCH 9/9] fix: retrigger CI