@planning-inspectorate/dynamic-forms 3.15.2 → 3.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/dynamic-forms",
3
- "version": "3.15.2",
3
+ "version": "3.15.4",
4
4
  "description": "This package is for building GDS forms using a configuration-based approach.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -56,6 +56,8 @@
56
56
  text: inputField.hint
57
57
  },
58
58
  classes: inputField.classes or "govuk-input-!-width-three-quarters",
59
+ inputmode: inputField.inputmode,
60
+ pattern: inputField.pattern,
59
61
  attributes: inputField.attributes
60
62
  }) }}
61
63
  {% endfor %}
@@ -77,6 +77,8 @@ interface InputField {
77
77
  prefix?: Affix; // used to add a prefix to the field
78
78
  hint?: string;
79
79
  classes?: string;
80
+ inputmode?: 'decimal' | 'numeric';
81
+ pattern?: string; // Used for backwards-compatibility for older iOS devices in numeric input fields
80
82
  }
81
83
 
82
84
  /*
@@ -40,7 +40,11 @@ async function validateQuestion(questionObj, req, journeyResponse = {}) {
40
40
  const validationRules = validation.validate(questionObj, journeyResponse);
41
41
 
42
42
  if (validationRules instanceof Array) {
43
- await Promise.all(validationRules.map((validator) => validator.run(req)));
43
+ // Intentionally run validators sequentially to ensure
44
+ // errors are collected in the same order as the rules array
45
+ for (const validator of validationRules) {
46
+ await validator.run(req);
47
+ }
44
48
 
45
49
  const errors = validationResult(req);
46
50
  const mappedErrors = errors.mapped();
@@ -4,6 +4,7 @@ import validate from './validator.js';
4
4
  import RequiredValidator from './required-validator.js';
5
5
  import ValidOptionValidator from './valid-option-validator.js';
6
6
  import AddressValidator from './address-validator.js';
7
+ import DateValidator from '#src/validator/date-validator.js';
7
8
 
8
9
  describe('./src/dynamic-forms/validator/validator.js', () => {
9
10
  let mockRes;
@@ -242,4 +243,51 @@ describe('./src/dynamic-forms/validator/validator.js', () => {
242
243
  assert.strictEqual(postcode._errors.length, 0);
243
244
  assert.strictEqual(next.mock.callCount(), 1);
244
245
  });
246
+
247
+ it('should display errors from a multi-field array validator in correct order (e.g., day, month, year)', async () => {
248
+ const req = {
249
+ params: {
250
+ section: 1,
251
+ question: 1,
252
+ referenceId: 'abc'
253
+ },
254
+ body: {
255
+ field1_day: '32',
256
+ field1_month: '13',
257
+ field1_year: '99'
258
+ }
259
+ };
260
+
261
+ mockRes.locals.journey = {
262
+ getQuestionByParams: function () {
263
+ return {
264
+ validators: [new DateValidator('the date')],
265
+ fieldName: 'field1'
266
+ };
267
+ }
268
+ };
269
+
270
+ const next = mock.fn();
271
+ await validate(req, mockRes, next);
272
+
273
+ const { validationResult } = await import('express-validator');
274
+ const errors = validationResult(req);
275
+ const mappedErrors = errors.mapped();
276
+ const errorKeys = Object.keys(mappedErrors);
277
+
278
+ // Verify all three errors exist
279
+ assert.strictEqual(errorKeys.includes('field1_day'), true, 'Missing day error');
280
+ assert.strictEqual(errorKeys.includes('field1_month'), true, 'Missing month error');
281
+ assert.strictEqual(errorKeys.includes('field1_year'), true, 'Missing year error');
282
+
283
+ // Verify errors are in the correct order: day, month, year
284
+ const dayIndex = errorKeys.indexOf('field1_day');
285
+ const monthIndex = errorKeys.indexOf('field1_month');
286
+ const yearIndex = errorKeys.indexOf('field1_year');
287
+
288
+ assert.strictEqual(dayIndex < monthIndex, true, 'Day error should come before month error');
289
+ assert.strictEqual(monthIndex < yearIndex, true, 'Month error should come before year error');
290
+
291
+ assert.strictEqual(next.mock.callCount(), 1);
292
+ });
245
293
  });
package/test/questions.js CHANGED
@@ -125,8 +125,8 @@ export const questionProps = {
125
125
  url: 'companions',
126
126
  label: 'Companions',
127
127
  inputFields: [
128
- { fieldName: 'name', label: 'Name', type: COMPONENT_TYPES.TEXT_ENTRY },
129
- { fieldName: 'age', label: 'Age', type: COMPONENT_TYPES.NUMBER }
128
+ { fieldName: 'name', label: 'Name' },
129
+ { fieldName: 'age', label: 'Age', inputmode: 'numeric', pattern: '[0-9]*' }
130
130
  ]
131
131
  },
132
132
  nights: {
@@ -22,7 +22,7 @@
22
22
  <label class="govuk-label govuk-label" for="age">
23
23
  Age
24
24
  </label>
25
- <input class="govuk-input govuk-input-!-width-three-quarters" id="age" name="age" type="text">
25
+ <input class="govuk-input govuk-input-!-width-three-quarters" id="age" name="age" type="text" pattern="[0-9]*" inputmode="numeric">
26
26
  </div>
27
27
 
28
28
 
@@ -80,6 +80,18 @@ export async function createAppWithQuestions(ctx, options) {
80
80
  return server;
81
81
  }
82
82
 
83
+ /**
84
+ * Returns a mock value for an input field based on its properties.
85
+ * @param {Object} field - An input field descriptor (not a full question)
86
+ * @returns {string|number}
87
+ */
88
+ function mockInputFieldValue(field) {
89
+ if (field.inputmode === 'numeric' || field.pattern === '[0-9]*') {
90
+ return 1;
91
+ }
92
+ return 'test';
93
+ }
94
+
83
95
  /**
84
96
  * Build a valid payload for a question.
85
97
  * @param {import('../src/questions/question-props.js').QuestionProps} q
@@ -128,7 +140,7 @@ export function mockAnswerBody(q) {
128
140
  case COMPONENT_TYPES.MULTI_FIELD_INPUT: {
129
141
  const res = {};
130
142
  for (const field of q.inputFields) {
131
- res[field.fieldName] = mockAnswer(field);
143
+ res[field.fieldName] = mockInputFieldValue(field);
132
144
  }
133
145
  return res;
134
146
  }
@@ -191,7 +203,7 @@ export function mockAnswer(q) {
191
203
  if (q.inputFields) {
192
204
  let res = '';
193
205
  for (const field of q.inputFields) {
194
- res += mockAnswer(field) + '<br>';
206
+ res += mockInputFieldValue(field) + '<br>';
195
207
  }
196
208
  return res;
197
209
  }
@@ -26,6 +26,8 @@ export class MultiFieldInputQuestion extends Question {
26
26
  prefix?: import("../../questions/question-props.d.ts").Affix;
27
27
  hint?: string;
28
28
  classes?: string;
29
+ inputmode?: "decimal" | "numeric";
30
+ pattern?: string;
29
31
  }[];
30
32
  /**
31
33
  * returns the formatted answers values to be used to build task list elements
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/multi-field-input/question.js"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH;;GAEG;AACH;IACC;;OAEG;IACH,8CAFW,OAAO,qCAAqC,EAAE,6BAA6B,EAarF;IAJC,wEAA8B;IAchC;;;;;;;;;;;;QAOC;IAsBD;;;;;;;;;;;;;OAaG;IACH,wDAZW,OAAO,GAEL,KAAK,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,MAAM,EAAE;YACN,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE,MAAM,CAAC;YACb,kBAAkB,EAAE,MAAM,CAAC;SAC5B,CAAC;KACH,CAAC,CAkBJ;;CAyBD;;gCAvHY,OAAO,WAAW,EAAE,iBAAiB;sBACrC,OAAO,UAAU,EAAE,OAAO;8BAC1B,OAAO,mBAAmB,EAAE,eAAe;;;yBAP/B,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/multi-field-input/question.js"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH;;GAEG;AACH;IACC;;OAEG;IACH,8CAFW,OAAO,qCAAqC,EAAE,6BAA6B,EAarF;IAJC,wEAA8B;IAchC;;;;;;;;;;;;;;QAOC;IAsBD;;;;;;;;;;;;;OAaG;IACH,wDAZW,OAAO,GAEL,KAAK,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,MAAM,EAAE;YACN,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE,MAAM,CAAC;YACb,kBAAkB,EAAE,MAAM,CAAC;SAC5B,CAAC;KACH,CAAC,CAkBJ;;CAyBD;;gCAvHY,OAAO,WAAW,EAAE,iBAAiB;sBACrC,OAAO,UAAU,EAAE,OAAO;8BAC1B,OAAO,mBAAmB,EAAE,eAAe;;;yBAP/B,WAAW"}
@@ -77,6 +77,8 @@ interface InputField {
77
77
  prefix?: Affix; // used to add a prefix to the field
78
78
  hint?: string;
79
79
  classes?: string;
80
+ inputmode?: 'decimal' | 'numeric';
81
+ pattern?: string; // Used for backwards-compatibility for older iOS devices in numeric input fields
80
82
  }
81
83
 
82
84
  /*