Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / test / date / timezone.spec.ts
blob75a583713336209a4a722c8f41ade1f06b35ba9a
1 import { listTimeZones } from '@protontech/timezone-support';
3 import type { SimpleMap } from '@proton/shared/lib/interfaces';
5 import {
6     convertUTCDateTimeToZone,
7     convertZonedDateTimeToUTC,
8     getAbbreviatedTimezoneName,
9     getReadableCityTimezone,
10     getSupportedTimezone,
11     getTimezoneAndOffset,
12     guessTimezone,
13 } from '../../lib/date/timezone';
14 import { MANUAL_TIMEZONE_LINKS, unsupportedTimezoneLinks } from '../../lib/date/timezoneDatabase';
16 describe('convert utc', () => {
17     const obj = (year: number, month: number, day: number, hours = 0, minutes = 0, seconds = 0) => ({
18         year,
19         month,
20         day,
21         hours,
22         minutes,
23         seconds,
24     });
26     it('should convert a zoned time (Australia/Sydney) to utc', () => {
27         expect(convertZonedDateTimeToUTC(obj(2019, 1, 1), 'Australia/Sydney')).toEqual(obj(2018, 12, 31, 13));
28     });
30     it('should convert a zoned time (Europe/Zurich summer) to utc', () => {
31         expect(convertZonedDateTimeToUTC(obj(2019, 6, 15, 1), 'Europe/Zurich')).toEqual(obj(2019, 6, 14, 23));
32     });
34     it('should convert a zoned time (Europe/Zurich winter) to utc', () => {
35         expect(convertZonedDateTimeToUTC(obj(2019, 12, 15, 1), 'Europe/Zurich')).toEqual(obj(2019, 12, 15, 0));
36     });
38     it('should convert a zoned time to utc with winter-to-summer (Europe/Zurich 2017) dst shift', () => {
39         expect(convertZonedDateTimeToUTC(obj(2017, 3, 26, 1), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 0));
40         expect(convertZonedDateTimeToUTC(obj(2017, 3, 26, 2), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 1));
41         expect(convertZonedDateTimeToUTC(obj(2017, 3, 26, 3), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 1));
42         expect(convertZonedDateTimeToUTC(obj(2017, 3, 26, 4), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 2));
43     });
45     it('should convert a zoned time to utc with summer-to-winter (Europe/Zurich 2019) dst shift', () => {
46         expect(convertZonedDateTimeToUTC(obj(2019, 10, 27, 1), 'Europe/Zurich')).toEqual(obj(2019, 10, 26, 23));
47         expect(convertZonedDateTimeToUTC(obj(2019, 10, 27, 2), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 1));
48         expect(convertZonedDateTimeToUTC(obj(2019, 10, 27, 3), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 2));
49         expect(convertZonedDateTimeToUTC(obj(2019, 10, 27, 4), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 3));
50     });
52     it('should convert from utc time to a timezone (Australia/Sydney)', () => {
53         expect(convertUTCDateTimeToZone(obj(2018, 12, 31, 13), 'Australia/Sydney')).toEqual(obj(2019, 1, 1));
54     });
56     it('should convert back from a utc to a timezone (Europe/Zurich summer)', () => {
57         expect(convertUTCDateTimeToZone(obj(2019, 6, 14, 23), 'Europe/Zurich')).toEqual(obj(2019, 6, 15, 1));
58     });
60     it('should convert back from a utc time to a timezone (Europe/Zurich winter)', () => {
61         expect(convertUTCDateTimeToZone(obj(2019, 12, 15, 0), 'Europe/Zurich')).toEqual(obj(2019, 12, 15, 1));
62     });
64     it('should convert back from a utc to a timezone (Europe/Zurich 2017) with summer-to-winter dst shifts', () => {
65         expect(convertUTCDateTimeToZone(obj(2017, 3, 26, 0), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 1));
66         expect(convertUTCDateTimeToZone(obj(2017, 3, 26, 1), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 3));
67         expect(convertUTCDateTimeToZone(obj(2017, 3, 26, 2), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 4));
68     });
70     it('should convert back from a utc to a timezone (Europe/Zurich 2017) with winter-to-summer dst shifts', () => {
71         expect(convertUTCDateTimeToZone(obj(2019, 10, 27, 0), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 2));
72         expect(convertUTCDateTimeToZone(obj(2019, 10, 27, 1), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 2));
73         expect(convertUTCDateTimeToZone(obj(2019, 10, 27, 2), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 3));
74     });
75 });
77 describe('getSupportedTimezone', () => {
78     it('should remove extra slashes', () => {
79         expect(getSupportedTimezone('/Europe/London/')).toEqual('Europe/London');
80         expect(getSupportedTimezone('/Africa/Freetown/')).toEqual('Africa/Abidjan');
81     });
83     it('should support the bluejeans format', () => {
84         expect(getSupportedTimezone('/tzurl.org/2020b/America/Chicago')).toEqual('America/Chicago');
85         expect(getSupportedTimezone('/tzurl.org/2020b/Asia/Istanbul')).toEqual('Europe/Istanbul');
86     });
88     it('should support the mozilla format', () => {
89         expect(getSupportedTimezone('/mozilla.org/20050126_1/America/Argentina/Ushuaia')).toEqual(
90             'America/Argentina/Ushuaia'
91         );
92         expect(getSupportedTimezone('/mozilla.org/20050126_1/America/Argentina/ComodRivadavia')).toEqual(
93             'America/Argentina/Catamarca'
94         );
95     });
97     it('should transform globally defined alias timezones according to the longest match', () => {
98         // GB (Europe/London), Eire (Europe/Dublin) and GB-Eire (Europe/London) are all alias timezones.
99         // getSupportedTimezone should transform according to the longest match, GB-Eire -> Europe/London
100         expect(getSupportedTimezone('/mozilla.org/20050126_1/GB-Eire')).toEqual('Europe/London');
101         expect(getSupportedTimezone('/tzurl.org/2021a/Eire')).toEqual('Europe/Dublin');
102         expect(getSupportedTimezone('/GB/')).toEqual('Europe/London');
103     });
105     it('should be robust for capturing globally defined timezones (for which no specification exists)', () => {
106         expect(getSupportedTimezone('/IANA-db:Asia/Seoul--custom.format')).toEqual('Asia/Seoul');
107     });
109     it('should filter non-supported canonical timezones', () => {
110         const canonical = listTimeZones();
111         const results = canonical.map((tzid) => getSupportedTimezone(tzid));
112         const expected = canonical.map((tzid) => unsupportedTimezoneLinks[tzid] || tzid);
113         expect(results).toEqual(expected);
114     });
116     it('should return the supported canonical timezone for alias timezones', () => {
117         const alias = [
118             'Africa/Bujumbura',
119             'Africa/Freetown',
120             'America/Antigua',
121             'America/Indianapolis',
122             'Asia/Macao',
123             'Asia/Istanbul',
124             'Europe/Skopje',
125             'GB-Eire',
126         ];
127         const canonical = [
128             'Africa/Maputo',
129             'Africa/Abidjan',
130             'America/Puerto_Rico',
131             'America/New_York',
132             'Asia/Macau',
133             'Europe/Istanbul',
134             'Europe/Belgrade',
135             'Europe/London',
136         ];
137         const results = alias.map((tzid) => getSupportedTimezone(tzid));
138         expect(results).toEqual(canonical);
139     });
141     it('should convert manual timezones', () => {
142         const manual = Object.keys(MANUAL_TIMEZONE_LINKS);
143         const results = manual.map((tzid) => getSupportedTimezone(tzid));
144         const expected = Object.values(MANUAL_TIMEZONE_LINKS);
145         expect(results).toEqual(expected);
146     });
148     it('should convert time zones we do not support yet', () => {
149         const expectedMap: SimpleMap<string> = {
150             'Pacific/Kanton': 'Pacific/Fakaofo',
151             'Europe/Kyiv': 'Europe/Kiev',
152             'America/Nuuk': 'Atlantic/Stanley',
153             'America/Ciudad_Juarez': 'America/Ojinaga',
154         };
156         Object.keys(expectedMap).forEach((tzid) => {
157             expect(getSupportedTimezone(tzid)).toBe(expectedMap[tzid]);
158         });
159     });
161     it('should convert deprecated time zones', () => {
162         const expectedMap: SimpleMap<string> = {
163             'Australia/Currie': 'Australia/Hobart',
164         };
166         Object.keys(expectedMap).forEach((tzid) => {
167             expect(getSupportedTimezone(tzid)).toBe(expectedMap[tzid]);
168         });
169     });
171     it('should be robust', () => {
172         const tzids = ['Chamorro (UTC+10)', '(GMT-01:00) Azores', 'Mountain Time (U.S. & Canada)'];
173         const expected = ['Pacific/Saipan', 'Atlantic/Azores', 'America/Denver'];
174         const results = tzids.map(getSupportedTimezone);
175         expect(results).toEqual(expected);
176     });
178     it('should return undefined for unknown timezones', () => {
179         const unknown = ['Chamorro Standard Time', '(UTC+01:00) Bruxelles, Copenhague, Madrid, Paris'];
180         const results = unknown.map((tzid) => getSupportedTimezone(tzid));
181         const expected = unknown.map(() => undefined);
182         expect(results).toEqual(expected);
183     });
186 describe('guessTimeZone', () => {
187     const timeZoneList = listTimeZones();
189     describe('should guess right when the browser detects "Europe/Zurich"', () => {
190         beforeEach(() => {
191             spyOn<any>(Intl, 'DateTimeFormat').and.returnValue({
192                 resolvedOptions: () => ({ timeZone: 'Europe/Zurich' }),
193             });
194         });
196         it('guesses "Europe/Zurich"', () => {
197             expect(guessTimezone(timeZoneList)).toBe('Europe/Zurich');
198         });
199     });
201     describe('should guess right when the browser detects "Europe/Kyiv"', () => {
202         beforeEach(() => {
203             spyOn<any>(Intl, 'DateTimeFormat').and.returnValue({
204                 resolvedOptions: () => ({ timeZone: 'Europe/Kyiv' }),
205             });
206         });
208         it('guesses "Europe/Kiev"', () => {
209             expect(guessTimezone(timeZoneList)).toBe('Europe/Kiev');
210         });
211     });
213     describe('should guess right when the browser detects "Pacific/Kanton"', () => {
214         beforeEach(() => {
215             spyOn<any>(Intl, 'DateTimeFormat').and.returnValue({
216                 resolvedOptions: () => ({ timeZone: 'Pacific/Kanton' }),
217             });
218         });
220         it('guesses "Pacific/Fakaofo"', () => {
221             expect(guessTimezone(timeZoneList)).toBe('Pacific/Fakaofo');
222         });
223     });
225     describe('should guess right when the browser detects unknown time zone with GMT+1', () => {
226         beforeEach(() => {
227             const baseTime = Date.UTC(2023, 2, 7, 9);
229             spyOn<any>(Intl, 'DateTimeFormat').and.returnValue({
230                 resolvedOptions: () => ({ timeZone: 'unknown' }),
231             });
232             spyOn<any>(window, 'Date').and.returnValue({
233                 getTime: () => baseTime,
234                 getTimezoneOffset: () => -60,
235                 getUTCFullYear: () => 2023,
236                 getUTCMonth: () => 2,
237                 getUTCDate: () => 7,
238                 getUTCDay: () => 2,
239                 getUTCHours: () => 9,
240                 getUTCMinutes: () => 0,
241                 getUTCSeconds: () => 0,
242                 getUTCMilliseconds: () => 0,
243             });
244         });
246         it('guesses "Africa/Algiers", first in the list with that GMT offset', () => {
247             expect(guessTimezone(timeZoneList)).toBe('Africa/Algiers');
248         });
249     });
252 describe('getReadableCityTimezone', () => {
253     it('should return city timezone without underscores', () => {
254         const cityTimezone = 'Los_Angeles';
256         expect(getReadableCityTimezone(cityTimezone)).toEqual('Los Angeles');
257     });
259     it('should return city timezone without change', () => {
260         const cityTimezone = 'Paris';
262         expect(getReadableCityTimezone(cityTimezone)).toEqual('Paris');
263     });
266 describe('getAbbreviatedTimezoneName', () => {
267     it('should return the expected offset timezone', () => {
268         const timezone = 'Europe/Paris';
269         const result = getAbbreviatedTimezoneName('offset', timezone) || '';
271         expect(['GMT+1', 'GMT+2']).toContain(result);
272     });
274     it('should return the expected city timezone', () => {
275         const timezone = 'Europe/Paris';
277         expect(getAbbreviatedTimezoneName('city', timezone)).toEqual('Paris');
278     });
280     it('should return the expected city timezone on a readable format', () => {
281         const timezone = 'America/Los_Angeles';
283         expect(getAbbreviatedTimezoneName('city', timezone)).toEqual('Los Angeles');
284     });
286     it('should return the expected city timezone for a longer timezone', () => {
287         const timezone = 'America/North_Dakota/New_Salem';
289         expect(getAbbreviatedTimezoneName('city', timezone)).toEqual('New Salem');
290     });
293 describe('getTimezoneAndOffset', () => {
294     it('should return the expected timezone string, containing offset and name', () => {
295         const timezone = 'Europe/Paris';
296         const result = getTimezoneAndOffset(timezone);
298         expect(['GMT+1 • Europe/Paris', 'GMT+2 • Europe/Paris']).toContain(result);
299     });