@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 +1 -1
- package/src/components/multi-field-input/index.njk +2 -0
- package/src/questions/question-props.d.ts +2 -0
- package/src/validator/validator.js +5 -1
- package/src/validator/validator.test.js +48 -0
- package/test/questions.js +2 -2
- package/test/snapshots/companions.html +1 -1
- package/test/utils/question-test-utils.js +14 -2
- package/types/src/components/multi-field-input/question.d.ts +2 -0
- package/types/src/components/multi-field-input/question.d.ts.map +1 -1
- package/types/src/questions/question-props.d.ts +2 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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'
|
|
129
|
-
{ fieldName: 'age', label: 'Age',
|
|
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] =
|
|
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 +=
|
|
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
|
|
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
|
/*
|