Skip to content

Commit 2bef5e0

Browse files
added fix for validation failure in functional test, also increased unit test coverage for sonar
1 parent aa9411a commit 2bef5e0

File tree

2 files changed

+252
-3
lines changed

2 files changed

+252
-3
lines changed

projects/ccd-case-ui-toolkit/src/lib/components/form/date-input/date-input-component.spec.ts

Lines changed: 246 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
});

projects/ccd-case-ui-toolkit/src/lib/components/form/date-input/date-input.component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,12 @@ export class DateInputComponent implements ControlValueAccessor, Validator, OnIn
247247
const localDateTimeString = `${date}T${time}.000`;
248248

249249
// convert from local time to UTC for storage
250-
const localMoment = moment(localDateTimeString);
250+
const localMoment = moment(localDateTimeString, 'YYYY-MM-DDTHH:mm:ss.SSS', true);
251+
252+
// if invalid, return the raw string for validation to catch
253+
if (!localMoment.isValid()) {
254+
return localDateTimeString;
255+
}
251256
const utcMoment = localMoment.utc();
252257

253258
// return in the expected format

0 commit comments

Comments
 (0)