@@ -162,6 +162,280 @@ describe('DatePipe', () => {
162162 (two hours behind or in front) when moving through the various steps.
163163 */
164164
165+ describe ( 'British Summer Time (BST) Transitions - EXUI-3066' , ( ) => {
166+ // BST starts: Last Sunday in March at 1:00 AM GMT → 2:00 AM BST (UTC+1)
167+ // BST ends: Last Sunday in October at 2:00 AM BST → 1:00 AM GMT (UTC+0)
168+
169+ describe ( 'Spring Forward - Clocks go forward (GMT to BST)' , ( ) => {
170+ it ( 'should correctly display UTC time just before spring forward transition' , ( ) => {
171+ // March 26, 2023 at 00:30 UTC (30 mins before clocks spring forward)
172+ const UTC_BEFORE_SPRING = '2023-03-26T00:30:00.000' ;
173+ const result = datePipe . transform ( UTC_BEFORE_SPRING , 'local' , null ) ;
174+
175+ // In local time (GMT), this would still be 00:30 AM on March 26
176+ expect ( result ) . toContain ( '26 Mar 2023' ) ;
177+ expect ( result ) . toContain ( '12:30:00 AM' ) ;
178+ } ) ;
179+
180+ it ( 'should correctly display UTC time just after spring forward transition' , ( ) => {
181+ // March 26, 2023 at 01:30 UTC (30 mins after clocks spring forward)
182+ // At 1:00 AM GMT, clocks jumped to 2:00 AM BST
183+ const UTC_AFTER_SPRING = '2023-03-26T01:30:00.000' ;
184+ const result = datePipe . transform ( UTC_AFTER_SPRING , 'local' , null ) ;
185+
186+ // In local time (BST = UTC+1), 01:30 UTC should be 02:30 BST
187+ expect ( result ) . toContain ( '26 Mar 2023' ) ;
188+ expect ( result ) . toContain ( '2:30:00 AM' ) ;
189+ } ) ;
190+
191+ it ( 'should handle UTC time during the missing hour (1:00-2:00 AM GMT)' , ( ) => {
192+ // March 26, 2023 at 01:15 UTC (during the non-existent hour in local time)
193+ const UTC_MISSING_HOUR = '2023-03-26T01:15:00.000' ;
194+ const result = datePipe . transform ( UTC_MISSING_HOUR , 'local' , null ) ;
195+
196+ // This time doesn't exist in local time, but moment converts it to BST
197+ // 01:15 UTC = 02:15 BST
198+ expect ( result ) . toContain ( '26 Mar 2023' ) ;
199+ expect ( result ) . toContain ( '2:15:00 AM' ) ;
200+ } ) ;
201+
202+ it ( 'should handle date-only value during spring forward day' , ( ) => {
203+ // Date without time during spring forward should not shift
204+ const SPRING_FORWARD_DATE = '2023-03-26' ;
205+ const result = datePipe . transform ( SPRING_FORWARD_DATE , null , null ) ;
206+
207+ expect ( result ) . toBe ( '26 Mar 2023' ) ;
208+ } ) ;
209+
210+ it ( 'should correctly handle noon UTC on spring forward day' , ( ) => {
211+ // March 26, 2023 at 12:00 UTC (well after transition)
212+ const UTC_NOON = '2023-03-26T12:00:00.000' ;
213+ const result = datePipe . transform ( UTC_NOON , 'local' , null ) ;
214+
215+ // 12:00 UTC = 13:00 BST (1:00 PM)
216+ expect ( result ) . toContain ( '26 Mar 2023' ) ;
217+ expect ( result ) . toContain ( '1:00:00 PM' ) ;
218+ } ) ;
219+ } ) ;
220+
221+ describe ( 'Fall Back - Clocks go back (BST to GMT)' , ( ) => {
222+ it ( 'should correctly display UTC time just before fall back transition' , ( ) => {
223+ // October 29, 2023 at 00:30 UTC (30 mins before clocks fall back)
224+ const UTC_BEFORE_FALL = '2023-10-29T00:30:00.000' ;
225+ const result = datePipe . transform ( UTC_BEFORE_FALL , 'local' , null ) ;
226+
227+ // Still in BST, so 00:30 UTC = 01:30 BST
228+ expect ( result ) . toContain ( '29 Oct 2023' ) ;
229+ expect ( result ) . toContain ( '1:30:00 AM' ) ;
230+ } ) ;
231+
232+ it ( 'should correctly display UTC time just after fall back transition' , ( ) => {
233+ // October 29, 2023 at 01:30 UTC (after clocks fell back)
234+ // At 2:00 AM BST, clocks went back to 1:00 AM GMT
235+ const UTC_AFTER_FALL = '2023-10-29T01:30:00.000' ;
236+ const result = datePipe . transform ( UTC_AFTER_FALL , 'local' , null ) ;
237+
238+ // Now in GMT, so 01:30 UTC = 01:30 GMT
239+ expect ( result ) . toContain ( '29 Oct 2023' ) ;
240+ expect ( result ) . toContain ( '1:30:00 AM' ) ;
241+ } ) ;
242+
243+ it ( 'should handle UTC time during the repeated hour (1:00-2:00 AM)' , ( ) => {
244+ // October 29, 2023 at 01:15 UTC (during the hour that occurs twice)
245+ const UTC_REPEATED_HOUR = '2023-10-29T01:15:00.000' ;
246+ const result = datePipe . transform ( UTC_REPEATED_HOUR , 'local' , null ) ;
247+
248+ // Moment will interpret this as GMT (after the transition)
249+ expect ( result ) . toContain ( '29 Oct 2023' ) ;
250+ expect ( result ) . toContain ( '1:15:00 AM' ) ;
251+ } ) ;
252+
253+ it ( 'should handle date-only value during fall back day' , ( ) => {
254+ // Date without time during fall back should not shift
255+ const FALL_BACK_DATE = '2023-10-29' ;
256+ const result = datePipe . transform ( FALL_BACK_DATE , null , null ) ;
257+
258+ expect ( result ) . toBe ( '29 Oct 2023' ) ;
259+ } ) ;
260+
261+ it ( 'should correctly handle noon UTC on fall back day' , ( ) => {
262+ // October 29, 2023 at 12:00 UTC (well after transition)
263+ const UTC_NOON = '2023-10-29T12:00:00.000' ;
264+ const result = datePipe . transform ( UTC_NOON , 'local' , null ) ;
265+
266+ // 12:00 UTC = 12:00 GMT (after fall back)
267+ expect ( result ) . toContain ( '29 Oct 2023' ) ;
268+ expect ( result ) . toContain ( '12:00:00 PM' ) ;
269+ } ) ;
270+ } ) ;
271+
272+ describe ( 'Regular BST and GMT periods' , ( ) => {
273+ it ( 'should correctly display UTC time during summer (BST period)' , ( ) => {
274+ // July 15, 2023 at 14:30 UTC (middle of summer, BST in effect)
275+ const UTC_SUMMER = '2023-07-15T14:30:00.000' ;
276+ const result = datePipe . transform ( UTC_SUMMER , 'local' , null ) ;
277+
278+ // 14:30 UTC = 15:30 BST (3:30 PM)
279+ expect ( result ) . toContain ( '15 Jul 2023' ) ;
280+ expect ( result ) . toContain ( '3:30:00 PM' ) ;
281+ } ) ;
282+
283+ it ( 'should correctly display UTC time during winter (GMT period)' , ( ) => {
284+ // January 15, 2023 at 14:30 UTC (middle of winter, GMT in effect)
285+ const UTC_WINTER = '2023-01-15T14:30:00.000' ;
286+ const result = datePipe . transform ( UTC_WINTER , 'local' , null ) ;
287+
288+ // 14:30 UTC = 14:30 GMT (2:30 PM)
289+ expect ( result ) . toContain ( '15 Jan 2023' ) ;
290+ expect ( result ) . toContain ( '2:30:00 PM' ) ;
291+ } ) ;
292+
293+ it ( 'should correctly display UTC midnight during BST' , ( ) => {
294+ // August 1, 2023 at 00:00 UTC (midnight during BST)
295+ const UTC_MIDNIGHT_BST = '2023-08-01T00:00:00.000' ;
296+ const result = datePipe . transform ( UTC_MIDNIGHT_BST , 'local' , null ) ;
297+
298+ // 00:00 UTC = 01:00 BST (1:00 AM)
299+ expect ( result ) . toContain ( '1 Aug 2023' ) ;
300+ expect ( result ) . toContain ( '1:00:00 AM' ) ;
301+ } ) ;
302+
303+ it ( 'should correctly display UTC midnight during GMT' , ( ) => {
304+ // December 1, 2023 at 00:00 UTC (midnight during GMT)
305+ const UTC_MIDNIGHT_GMT = '2023-12-01T00:00:00.000' ;
306+ const result = datePipe . transform ( UTC_MIDNIGHT_GMT , 'local' , null ) ;
307+
308+ // 00:00 UTC = 00:00 GMT (midnight)
309+ expect ( result ) . toContain ( '1 Dec 2023' ) ;
310+ expect ( result ) . toContain ( '12:00:00 AM' ) ;
311+ } ) ;
312+
313+ it ( 'should correctly display UTC 23:00 during BST (crosses day boundary)' , ( ) => {
314+ // July 15, 2023 at 23:30 UTC (late evening during BST)
315+ const UTC_LATE_BST = '2023-07-15T23:30:00.000' ;
316+ const result = datePipe . transform ( UTC_LATE_BST , 'local' , null ) ;
317+
318+ // 23:30 UTC = 00:30 next day BST (crosses midnight)
319+ expect ( result ) . toContain ( '16 Jul 2023' ) ;
320+ expect ( result ) . toContain ( '12:30:00 AM' ) ;
321+ } ) ;
322+ } ) ;
323+
324+ describe ( 'Edge cases with timezone offset notation' , ( ) => {
325+ it ( 'should handle UTC datetime with Z suffix during BST period' , ( ) => {
326+ const UTC_WITH_Z = '2023-06-15T10:30:00.000Z' ;
327+ const result = datePipe . transform ( UTC_WITH_Z , 'local' , null ) ;
328+
329+ // 10:30 UTC = 11:30 BST
330+ expect ( result ) . toContain ( '15 Jun 2023' ) ;
331+ expect ( result ) . toContain ( '11:30:00 AM' ) ;
332+ } ) ;
333+
334+ it ( 'should handle UTC datetime with +00:00 offset during GMT period' , ( ) => {
335+ const UTC_WITH_OFFSET = '2023-01-15T10:30:00.000+00:00' ;
336+ const result = datePipe . transform ( UTC_WITH_OFFSET , 'local' , null ) ;
337+
338+ // 10:30 UTC = 10:30 GMT
339+ expect ( result ) . toContain ( '15 Jan 2023' ) ;
340+ expect ( result ) . toContain ( '10:30:00 AM' ) ;
341+ } ) ;
342+ } ) ;
343+
344+ describe ( 'UTC mode (should not convert to local time)' , ( ) => {
345+ it ( 'should keep UTC time as-is when zone is utc during BST period' , ( ) => {
346+ const UTC_TIME = '2023-07-15T14:30:00.000' ;
347+ const result = datePipe . transform ( UTC_TIME , 'utc' , null ) ;
348+
349+ // Should remain 14:30, not converted to BST
350+ expect ( result ) . toContain ( '15 Jul 2023' ) ;
351+ expect ( result ) . toContain ( '2:30:00 PM' ) ;
352+ } ) ;
353+
354+ it ( 'should keep UTC time as-is when zone is utc during GMT period' , ( ) => {
355+ const UTC_TIME = '2023-01-15T14:30:00.000' ;
356+ const result = datePipe . transform ( UTC_TIME , 'utc' , null ) ;
357+
358+ // Should remain 14:30
359+ expect ( result ) . toContain ( '15 Jan 2023' ) ;
360+ expect ( result ) . toContain ( '2:30:00 PM' ) ;
361+ } ) ;
362+ } ) ;
363+ } ) ;
364+
365+ describe ( 'Edge case tests - EXUI-3066' , ( ) => {
366+ it ( 'should correctly handle timezone offset notation (+01:00)' , ( ) => {
367+ const DATE_WITH_OFFSET = '2023-07-15T14:30:00.000+01:00' ;
368+ const result = datePipe . transform ( DATE_WITH_OFFSET , 'local' , null ) ;
369+
370+ // 14:30 in +01:00 timezone = 13:30 UTC = 14:30 BST (in UK summer)
371+ expect ( result ) . toContain ( '15 Jul 2023' ) ;
372+ expect ( result ) . toContain ( '2:30:00 PM' ) ;
373+ } ) ;
374+
375+ it ( 'should correctly handle negative timezone offsets (-05:00)' , ( ) => {
376+ const DATE_WITH_NEG_OFFSET = '2023-01-15T10:00:00.000-05:00' ;
377+ const result = datePipe . transform ( DATE_WITH_NEG_OFFSET , 'local' , null ) ;
378+
379+ // 10:00 in -05:00 (EST) = 15:00 UTC = 15:00 GMT (in UK winter)
380+ expect ( result ) . toContain ( '15 Jan 2023' ) ;
381+ expect ( result ) . toContain ( '3:00:00 PM' ) ;
382+ } ) ;
383+
384+ it ( 'should handle datetime values crossing day boundary when converting to local' , ( ) => {
385+ const LATE_UTC = '2023-08-15T23:30:00.000' ;
386+ const result = datePipe . transform ( LATE_UTC , 'local' , null ) ;
387+
388+ // 23:30 UTC in summer = 00:30 next day BST
389+ expect ( result ) . toContain ( '16 Aug 2023' ) ;
390+ expect ( result ) . toContain ( '12:30:00 AM' ) ;
391+ } ) ;
392+
393+ it ( 'should handle early morning UTC times during winter' , ( ) => {
394+ const EARLY_UTC_WINTER = '2023-01-15T00:30:00.000' ;
395+ const result = datePipe . transform ( EARLY_UTC_WINTER , 'local' , null ) ;
396+
397+ // 00:30 UTC in winter = 00:30 GMT (no change)
398+ expect ( result ) . toContain ( '15 Jan 2023' ) ;
399+ expect ( result ) . toContain ( '12:30:00 AM' ) ;
400+ } ) ;
401+
402+ it ( 'should handle early morning UTC times during summer (stays on same day)' , ( ) => {
403+ const EARLY_UTC_SUMMER = '2023-07-15T00:30:00.000' ;
404+ const result = datePipe . transform ( EARLY_UTC_SUMMER , 'local' , null ) ;
405+
406+ // 00:30 UTC in summer = 01:30 BST (same day)
407+ expect ( result ) . toContain ( '15 Jul 2023' ) ;
408+ expect ( result ) . toContain ( '1:30:00 AM' ) ;
409+ } ) ;
410+
411+ it ( 'should correctly handle DST transition with explicit UTC offset' , ( ) => {
412+ const DST_TRANSITION_WITH_OFFSET = '2023-03-26T01:00:00.000+00:00' ;
413+ const result = datePipe . transform ( DST_TRANSITION_WITH_OFFSET , 'local' , null ) ;
414+
415+ // 01:00 UTC with +00:00 = 02:00 BST (after transition)
416+ expect ( result ) . toContain ( '26 Mar 2023' ) ;
417+ expect ( result ) . toContain ( '2:00:00 AM' ) ;
418+ } ) ;
419+
420+ it ( 'should handle timezone-aware string when converting to UTC mode' , ( ) => {
421+ const WITH_OFFSET = '2023-06-15T14:30:00.000+02:00' ;
422+ const result = datePipe . transform ( WITH_OFFSET , 'utc' , null ) ;
423+
424+ // 14:30 in +02:00 = 12:30 UTC
425+ expect ( result ) . toContain ( '15 Jun 2023' ) ;
426+ expect ( result ) . toContain ( '12:30:00 PM' ) ;
427+ } ) ;
428+
429+ it ( 'should handle datetime at exact DST transition moment' , ( ) => {
430+ const EXACT_TRANSITION = '2023-03-26T01:00:00.000' ;
431+ const result = datePipe . transform ( EXACT_TRANSITION , 'local' , null ) ;
432+
433+ // At 1:00 AM GMT, clocks jump to 2:00 AM BST
434+ expect ( result ) . toContain ( '26 Mar 2023' ) ;
435+ expect ( result ) . toContain ( '2:00:00 AM' ) ;
436+ } ) ;
437+ } ) ;
438+
165439 function getExpectedHour ( hour ) : number {
166440 let expectedHour = hour + EXPECTED_OFFSET ;
167441 if ( expectedHour > 12 ) {
0 commit comments