diff --git a/runner/src/server/forms/ReportAnOutbreak.json b/runner/src/server/forms/ReportAnOutbreak.json index 2982bfae96..095e6b3ea4 100644 --- a/runner/src/server/forms/ReportAnOutbreak.json +++ b/runner/src/server/forms/ReportAnOutbreak.json @@ -440,8 +440,7 @@ "name": "S4Q6", "options": { "required": true, - "maxDaysInFuture": "", - "maxDaysInPast": "" + "maxDaysInFuture": "0" }, "type": "DatePartsField", "title": "S4Q6. Symptom onset date of first case", @@ -453,7 +452,7 @@ "name": "S4Q7", "options": { "required": false, - "maxDaysInFuture": "" + "maxDaysInFuture": "0" }, "type": "DatePartsField", "title": "S4Q7. Symptom onset date of second case", @@ -465,7 +464,7 @@ "name": "S4Q8", "options": { "required": true, - "maxDaysInFuture": "" + "maxDaysInFuture": "0" }, "type": "DatePartsField", "title": "S4Q8. Symptom onset date of most recent case", @@ -714,7 +713,7 @@ "name": "S5Q12", "options": { "required": true, - "maxDaysInFuture": "" + "maxDaysInFuture": "0" }, "type": "DatePartsField", "title": "S5Q12. Symptom onset date of first case (confirmed flu, suspected flu OR chest infections)", @@ -726,7 +725,7 @@ "name": "S5Q13", "options": { "required": false, - "maxDaysInFuture": "" + "maxDaysInFuture": "0" }, "type": "DatePartsField", "title": "S5Q13. Symptom onset date of second case (confirmed flu, suspected flu OR chest infections)", @@ -738,7 +737,7 @@ "name": "S5Q14", "options": { "required": true, - "maxDaysInFuture": "" + "maxDaysInFuture": "0" }, "type": "DatePartsField", "title": "S5Q14. Symptom onset date of most recent case (confirmed flu, suspected flu OR chest infections)", @@ -882,7 +881,7 @@ "name": "S6Q4", "options": { "required": true, - "maxDaysInFuture": "" + "maxDaysInFuture": "0" }, "type": "DatePartsField", "title": "S6Q4. Symptom onset date of first case", @@ -894,7 +893,7 @@ "name": "S6Q5", "options": { "required": false, - "maxDaysInFuture": "" + "maxDaysInFuture": "0" }, "type": "DatePartsField", "title": "S6Q5. Symptom onset date of second case", @@ -906,7 +905,7 @@ "name": "S6Q6", "options": { "required": true, - "maxDaysInFuture": "" + "maxDaysInFuture": "0" }, "type": "DatePartsField", "title": "S6Q6. Symptom onset date of most recent case", diff --git a/runner/src/server/plugins/engine/components/DatePartsField.ts b/runner/src/server/plugins/engine/components/DatePartsField.ts index 9cdcec7ccd..c59455ffce 100644 --- a/runner/src/server/plugins/engine/components/DatePartsField.ts +++ b/runner/src/server/plugins/engine/components/DatePartsField.ts @@ -1,4 +1,4 @@ -import { add, sub, parseISO, format } from "date-fns"; +import { parseISO, format } from "date-fns"; import { InputFieldsComponentsDef } from "@xgovformbuilder/model"; import { FormComponent } from "./FormComponent"; @@ -21,11 +21,10 @@ export class DatePartsField extends FormComponent { constructor(def: InputFieldsComponentsDef, model: FormModel) { super(def, model); - const { name, options, title } = this; + const { name, options } = this; const isRequired = "required" in options && options.required === false ? false : true; const optionalText = "optionalText" in options && options.optionalText; - this.children = new ComponentCollection( [ { @@ -84,8 +83,9 @@ export class DatePartsField extends FormComponent { helpers.getCustomDateValidator(maxDaysInPast, maxDaysInFuture) ); - const returnValue = { [this.name]: schema }; - return returnValue; + this.schema = schema; + + return { [this.name]: schema }; } getFormDataFromState(state: FormSubmissionState) { @@ -102,14 +102,21 @@ export class DatePartsField extends FormComponent { getStateValueFromValidForm(payload: FormPayload) { const name = this.name; - - return payload[`${name}__year`] - ? new Date( - payload[`${name}__year`], - payload[`${name}__month`] - 1, - payload[`${name}__day`] - ) - : null; + const day = payload[`${name}__day`]; + const month = payload[`${name}__month`]; + const year = payload[`${name}__year`]; + + if (day || month || year) { + const indexedMonth = month - 1; // Adjust month for zero-based index + const parsedDate = new Date(year, indexedMonth, day); + + if (month - 1 === parsedDate.getMonth()) { + return parsedDate; + } else { + return new Date(0, 0, 0); // Invalid date fallback + } + } + return null; } getDisplayStringFromState(state: FormSubmissionState) { @@ -120,10 +127,6 @@ export class DatePartsField extends FormComponent { // @ts-ignore - eslint does not report this as an error, only tsc getViewModel(formData: FormData, errors: FormSubmissionErrors) { - const isRequired = - "required" in this.options && this.options.required === false - ? false - : true; const viewModel = super.getViewModel(formData, errors); // Use the component collection to generate the subitems @@ -137,54 +140,20 @@ export class DatePartsField extends FormComponent { optionalText, "" ) as any; - //componentViewModel.label = `DATE: ${new Date()}` as any; if (componentViewModel.errorMessage) { componentViewModel.classes += " govuk-input--error"; } }); - const firstError = errors?.errorList?.[0]; - //const errorMessage = isRequired && firstError && { text: firstError?.text }; - - let text = ""; - - let missingParts: any = []; - - if (formData[`${this.name}__day`] === "") { - missingParts.push("day"); - } - if (formData[`${this.name}__month`] === "") { - missingParts.push("month"); - } - if (formData[`${this.name}__year`] === "") { - missingParts.push("year"); - } - - if (missingParts.length === 3) { - text = `${this.title} is required.`; - } else if (missingParts.length > 0) { - text = `${this.title} must have a ${missingParts.join(", ")}.`; - } - - if (errors?.errorList?.length === 1) { - text = errors?.errorList?.[0].text; - } - - //if(formData[`${this.name}__day`]) - - if (errors?.errorList?.length === 1) { - text = errors?.errorList?.[0].text; - } - - if (!text.includes(this.title)) { - text = ""; - } + const relevantErrors = + errors?.errorList?.filter((error) => error.path.includes(this.name)) ?? + []; + const firstError = relevantErrors[0]; + const errorMessage = firstError && { text: firstError?.text }; - const errorMessage = isRequired && firstError && { text }; return { ...viewModel, - title: this.title, errorMessage, fieldset: { legend: viewModel.label, diff --git a/runner/src/server/plugins/engine/components/helpers.ts b/runner/src/server/plugins/engine/components/helpers.ts index 5e41276d15..435c240a09 100644 --- a/runner/src/server/plugins/engine/components/helpers.ts +++ b/runner/src/server/plugins/engine/components/helpers.ts @@ -102,6 +102,7 @@ export function getCustomDateValidator( }); } } + if (maxDaysInFuture) { const maxDate = add(startOfToday(), { days: maxDaysInFuture }); if (value > maxDate) { @@ -111,6 +112,13 @@ export function getCustomDateValidator( }); } } + + if (value.getFullYear() == 1899) { + return helpers.error("date.base", { + label: helpers.state.key, + }); + } + return value; }; } diff --git a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts index f21307f0d0..89c05a2397 100644 --- a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts +++ b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts @@ -326,7 +326,7 @@ export class PageControllerBase { href: `#${name}`, name: name.split("__")[0], title: title, - text: `${title} is required.`, + text: `${title} must be a valid date.`, }); } } else { diff --git a/runner/src/server/plugins/engine/pageControllers/validationOptions.ts b/runner/src/server/plugins/engine/pageControllers/validationOptions.ts index 20a6a97ec1..c3a2b975a4 100644 --- a/runner/src/server/plugins/engine/pageControllers/validationOptions.ts +++ b/runner/src/server/plugins/engine/pageControllers/validationOptions.ts @@ -8,7 +8,7 @@ const messageTemplate = { min: "{{#label}} must be at least {{#limit}} characters", regex: "enter a valid {{#label}}", email: "{{#label}} must be a valid email address", - date: "{{#label}} must be a date", + date: "{{#label}} must be a valid date", dateMin: "{{#label}} must be on or after {{#limit}}", dateMax: "{{#label}} must be on or before {{#limit}}", number: "{{#label}} must be a number", @@ -27,13 +27,11 @@ export const messages: ValidationOptions["messages"] = { "string.regex.base": messageTemplate.format, "string.maxWords": messageTemplate.maxWords, - "date.base": messageTemplate.date, "date.empty": messageTemplate.required, "date.required": messageTemplate.required, "date.min": messageTemplate.dateMin, "date.max": messageTemplate.dateMax, - "number.base": messageTemplate.number, "number.empty": messageTemplate.required, "number.required": messageTemplate.required, @@ -42,7 +40,6 @@ export const messages: ValidationOptions["messages"] = { "any.required": messageTemplate.required, "any.empty": messageTemplate.required, - }; export const validationOptions: ValidationOptions = { diff --git a/runner/test/cases/server/plugins/engine/datefield.test.ts b/runner/test/cases/server/plugins/engine/datefield.test.ts index 6ee9adff5b..e5c1213478 100644 --- a/runner/test/cases/server/plugins/engine/datefield.test.ts +++ b/runner/test/cases/server/plugins/engine/datefield.test.ts @@ -35,6 +35,22 @@ suite("Date field", () => { schema.validate("4000-40-40", { messages }).error.message ).to.contain("must be a valid date"); + expect( + schema.validate("2024-02-30", { messages }).error.message + ).to.contain("must be a valid date"); + + expect( + schema.validate("2023-02-29", { messages }).error.message + ).to.contain("must be a valid date"); + + expect(schema.validate("2023-11-", { messages }).error.message).to.contain( + "must be a valid date" + ); + + expect(schema.validate("", { messages }).error.message).to.contain( + "must be a valid date" + ); + expect(schema.validate("2021-12-25", { messages }).error).to.be.undefined(); });