@@ -96,6 +96,71 @@ describe('Date input component', () => {
9696 expect ( yearInput . value ) . toBe ( 'someRandomValue' ) ;
9797 } ) ;
9898
99+ it ( 'should convert UTC to local time when isDateTime is true and value contains T' , ( ) => {
100+ component . isDateTime = true ;
101+ // using a fixed UTC time: 2025-04-09T12:00:00.542Z (noon UTC)
102+ // in Europe/London timezone (BST UTC+1 in April), this becomes 13:00:00 local time
103+ component . writeValue ( '2025-04-09T12:00:00.542Z' ) ;
104+
105+ expect ( component . displayYear ) . toBeTruthy ( ) ;
106+ expect ( component . displayMonth ) . toBeTruthy ( ) ;
107+ expect ( component . displayDay ) . toBeTruthy ( ) ;
108+ expect ( component . displayHour ) . toBeTruthy ( ) ;
109+ expect ( component . displayMinute ) . toBeTruthy ( ) ;
110+ expect ( component . displaySecond ) . toBeTruthy ( ) ;
111+
112+ // verify the date parts are correct for Europe/London timezone
113+ expect ( component . displayYear ) . toBe ( '2025' ) ;
114+ expect ( component . displayMonth ) . toBe ( '04' ) ;
115+ expect ( component . displayDay ) . toBe ( '09' ) ;
116+ expect ( component . displayHour ) . toBe ( '13' ) ;
117+ expect ( component . displayMinute ) . toBe ( '00' ) ;
118+ expect ( component . displaySecond ) . toBe ( '00' ) ;
119+ } ) ;
120+
121+ it ( 'should handle DateTime fields without timezone indicator' , ( ) => {
122+ component . isDateTime = true ;
123+ // without timezone indicator, the value is treated as UTC and converted to local time
124+ // 2025-04-09T08:02:27.542 UTC becomes 09:02:27 in Europe/London (BST UTC+1)
125+ component . writeValue ( '2025-04-09T08:02:27.542' ) ;
126+
127+ expect ( component . displayYear ) . toBeTruthy ( ) ;
128+ expect ( component . displayMonth ) . toBeTruthy ( ) ;
129+ expect ( component . displayDay ) . toBeTruthy ( ) ;
130+ expect ( component . displayHour ) . toBeTruthy ( ) ;
131+ expect ( component . displayMinute ) . toBeTruthy ( ) ;
132+ expect ( component . displaySecond ) . toBeTruthy ( ) ;
133+
134+ // verify the UTC to local conversion for Europe/London timezone
135+ expect ( component . displayYear ) . toBe ( '2025' ) ;
136+ expect ( component . displayMonth ) . toBe ( '04' ) ;
137+ expect ( component . displayDay ) . toBe ( '09' ) ;
138+ expect ( component . displayHour ) . toBe ( '09' ) ;
139+ expect ( component . displayMinute ) . toBe ( '02' ) ;
140+ expect ( component . displaySecond ) . toBe ( '27' ) ;
141+ } ) ;
142+
143+ it ( 'should parse Date fields normally when isDateTime is false' , ( ) => {
144+ component . isDateTime = false ;
145+ component . writeValue ( '2021-04-09' ) ;
146+
147+ expect ( component . displayYear ) . toBe ( '2021' ) ;
148+ expect ( component . displayMonth ) . toBe ( '04' ) ;
149+ expect ( component . displayDay ) . toBe ( '09' ) ;
150+ } ) ;
151+
152+ it ( 'should parse Date fields with time part when isDateTime is false' , ( ) => {
153+ component . isDateTime = false ;
154+ component . writeValue ( '2021-04-09T08:02:27.542' ) ;
155+
156+ expect ( component . displayYear ) . toBe ( '2021' ) ;
157+ expect ( component . displayMonth ) . toBe ( '04' ) ;
158+ expect ( component . displayDay ) . toBe ( '09' ) ;
159+ expect ( component . displayHour ) . toBe ( '08' ) ;
160+ expect ( component . displayMinute ) . toBe ( '02' ) ;
161+ expect ( component . displaySecond ) . toBe ( '27' ) ;
162+ } ) ;
163+
99164 it ( 'should be valid when the date is in correct format' , ( ) => {
100165 const results = component . validate ( { value : DATE } as FormControl ) ;
101166 expect ( results ) . toBeUndefined ( ) ;
@@ -179,7 +244,7 @@ describe('Date input component', () => {
179244 } ) ;
180245
181246 describe ( 'year input component' , ( ) => {
182- it ( 'year input should null for a null value' , async ( ) => {
247+ it ( 'year input should be valid for a string value' , async ( ) => {
183248 component . id = 'yearInput' ;
184249 component . yearChange ( '2021' ) ;
185250 component . displayYear = '2021' ;
@@ -188,7 +253,7 @@ describe('Date input component', () => {
188253 expect ( input . value ) . toBe ( '2021' ) ;
189254 } ) ;
190255
191- it ( 'year input should null for a null value' , async ( ) => {
256+ it ( 'year input should be null for a null value' , async ( ) => {
192257 component . id = 'yearInput' ;
193258 component . yearChange ( null ) ;
194259 component . displayYear = null ;
@@ -245,4 +310,183 @@ describe('Date input component', () => {
245310 expect ( result ) . toBe ( 'start-year' ) ;
246311 } ) ;
247312 } ) ;
313+
314+ describe ( 'hour input component' , ( ) => {
315+ it ( 'hour input should be valid for a string value' , async ( ) => {
316+ component . id = 'hourInput' ;
317+ component . isDateTime = true ;
318+ component . hourChange ( '08' ) ;
319+ component . displayHour = '08' ;
320+ fixture . detectChanges ( ) ;
321+ const input = await de . query ( By . css ( `#${ component . hourId ( ) } ` ) ) . componentInstance ;
322+ expect ( input . value ) . toBe ( '08' ) ;
323+ } ) ;
324+
325+ it ( 'hour input should be null for a null value' , async ( ) => {
326+ component . id = 'hourInput' ;
327+ component . isDateTime = true ;
328+ component . hourChange ( null ) ;
329+ component . displayHour = null ;
330+ fixture . detectChanges ( ) ;
331+ const input = await de . query ( By . css ( `#${ component . hourId ( ) } ` ) ) . componentInstance ;
332+ expect ( input . value ) . toBeNull ( ) ;
333+ } ) ;
334+
335+ it ( 'should return the correct hourId' , ( ) => {
336+ component . id = 'startDateTime' ;
337+
338+ const result = component . hourId ( ) ;
339+
340+ expect ( result ) . toBe ( 'startDateTime-hour' ) ;
341+ } ) ;
342+ } ) ;
343+
344+ describe ( 'minute input component' , ( ) => {
345+ it ( 'minute input should be valid for a string value' , async ( ) => {
346+ component . id = 'minuteInput' ;
347+ component . isDateTime = true ;
348+ component . minuteChange ( '02' ) ;
349+ component . displayMinute = '02' ;
350+ fixture . detectChanges ( ) ;
351+ const input = await de . query ( By . css ( `#${ component . minuteId ( ) } ` ) ) . componentInstance ;
352+ expect ( input . value ) . toBe ( '02' ) ;
353+ } ) ;
354+
355+ it ( 'minute input should be null for a null value' , async ( ) => {
356+ component . id = 'minuteInput' ;
357+ component . isDateTime = true ;
358+ component . minuteChange ( null ) ;
359+ component . displayMinute = null ;
360+ fixture . detectChanges ( ) ;
361+ const input = await de . query ( By . css ( `#${ component . minuteId ( ) } ` ) ) . componentInstance ;
362+ expect ( input . value ) . toBeNull ( ) ;
363+ } ) ;
364+
365+ it ( 'should return the correct minuteId' , ( ) => {
366+ component . id = 'startDateTime' ;
367+
368+ const result = component . minuteId ( ) ;
369+
370+ expect ( result ) . toBe ( 'startDateTime-minute' ) ;
371+ } ) ;
372+ } ) ;
373+
374+ describe ( 'second input component' , ( ) => {
375+ it ( 'second input should be valid for a string value' , async ( ) => {
376+ component . id = 'secondInput' ;
377+ component . isDateTime = true ;
378+ component . secondChange ( '27' ) ;
379+ component . displaySecond = '27' ;
380+ fixture . detectChanges ( ) ;
381+ const input = await de . query ( By . css ( `#${ component . secondId ( ) } ` ) ) . componentInstance ;
382+ expect ( input . value ) . toBe ( '27' ) ;
383+ } ) ;
384+
385+ it ( 'second input should be null for a null value' , async ( ) => {
386+ component . id = 'secondInput' ;
387+ component . isDateTime = true ;
388+ component . secondChange ( null ) ;
389+ component . displaySecond = null ;
390+ fixture . detectChanges ( ) ;
391+ const input = await de . query ( By . css ( `#${ component . secondId ( ) } ` ) ) . componentInstance ;
392+ expect ( input . value ) . toBeNull ( ) ;
393+ } ) ;
394+
395+ it ( 'should return the correct secondId' , ( ) => {
396+ component . id = 'startDateTime' ;
397+
398+ const result = component . secondId ( ) ;
399+
400+ expect ( result ) . toBe ( 'startDateTime-second' ) ;
401+ } ) ;
402+ } ) ;
403+
404+ describe ( 'DateTime UTC conversion in viewValue' , ( ) => {
405+ beforeEach ( ( ) => {
406+ component . isDateTime = true ;
407+ component . id = 'dateTimeField' ;
408+ } ) ;
409+
410+ it ( 'should convert local time to UTC when isDateTime is true' , ( ) => {
411+ component . registerOnChange ( onChange ) ;
412+
413+ // set local time values (Europe/London BST is UTC+1 in April)
414+ // local time: 2025-04-09 08:02:27 BST
415+ component . yearChange ( '2025' ) ;
416+ component . monthChange ( '04' ) ;
417+ component . dayChange ( '09' ) ;
418+ component . hourChange ( '08' ) ;
419+ component . minuteChange ( '02' ) ;
420+ component . secondChange ( '27' ) ;
421+
422+ // The onChange should have been called with UTC time
423+ expect ( onChange ) . toHaveBeenCalled ( ) ;
424+ const calledValue = onChange . calls . mostRecent ( ) . args [ 0 ] ;
425+
426+ // verify correct conversion: 08:02:27 BST (UTC+1) should become 07:02:27 UTC
427+ expect ( calledValue ) . toBe ( '2025-04-09T07:02:27.000' ) ;
428+ } ) ;
429+
430+ it ( 'should return invalid date string when moment cannot parse it' , ( ) => {
431+ component . registerOnChange ( onChange ) ;
432+
433+ component . yearChange ( '2025' ) ;
434+ component . monthChange ( '13' ) ; // Invalid month
435+ component . dayChange ( '32' ) ; // Invalid day
436+ component . hourChange ( '08' ) ;
437+ component . minuteChange ( '02' ) ;
438+ component . secondChange ( '27' ) ;
439+
440+ // should still return the formatted string for validation to catch
441+ expect ( onChange ) . toHaveBeenCalled ( ) ;
442+ const calledValue = onChange . calls . mostRecent ( ) . args [ 0 ] ;
443+ expect ( calledValue ) . toBe ( '2025-13-32T08:02:27.000' ) ;
444+ } ) ;
445+
446+ it ( 'should pad single digit values correctly' , ( ) => {
447+ component . registerOnChange ( onChange ) ;
448+
449+ // set single digit values (Europe/London BST is UTC+1 in April)
450+ // local time: 2025-04-09 08:02:07 BST should become 2025-04-09 07:02:07 UTC
451+ component . yearChange ( '2025' ) ;
452+ component . monthChange ( '4' ) ; // Single digit
453+ component . dayChange ( '9' ) ; // Single digit
454+ component . hourChange ( '8' ) ; // Single digit
455+ component . minuteChange ( '2' ) ; // Single digit
456+ component . secondChange ( '7' ) ; // Single digit
457+
458+ expect ( onChange ) . toHaveBeenCalled ( ) ;
459+ const calledValue = onChange . calls . mostRecent ( ) . args [ 0 ] ;
460+
461+ // verify padding was applied and correct UTC conversion
462+ expect ( calledValue ) . toBe ( '2025-04-09T07:02:07.000' ) ;
463+ } ) ;
464+
465+ it ( 'should return null when no values are set' , ( ) => {
466+ component . registerOnChange ( onChange ) ;
467+ component . dayChange ( '' ) ;
468+
469+ const calledValue = onChange . calls . mostRecent ( ) . args [ 0 ] ;
470+ expect ( calledValue ) . toBeNull ( ) ;
471+ } ) ;
472+ } ) ;
473+
474+ describe ( 'Date fields without time conversion' , ( ) => {
475+ beforeEach ( ( ) => {
476+ component . isDateTime = false ;
477+ component . id = 'dateField' ;
478+ } ) ;
479+
480+ it ( 'should return date without time when isDateTime is false' , ( ) => {
481+ component . registerOnChange ( onChange ) ;
482+
483+ component . yearChange ( '2025' ) ;
484+ component . monthChange ( '04' ) ;
485+ component . dayChange ( '09' ) ;
486+
487+ expect ( onChange ) . toHaveBeenCalled ( ) ;
488+ const calledValue = onChange . calls . mostRecent ( ) . args [ 0 ] ;
489+ expect ( calledValue ) . toBe ( '2025-04-09' ) ;
490+ } ) ;
491+ } ) ;
248492} ) ;
0 commit comments