@planning-inspectorate/dynamic-forms 3.14.0 → 3.15.0
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/address/question.js +14 -0
- package/src/components/address/question.test.js +13 -0
- package/src/components/date/question.js +8 -0
- package/src/components/date/question.test.js +17 -0
- package/src/components/date-period/question.js +15 -0
- package/src/components/date-period/question.test.js +20 -0
- package/src/components/date-time/question.js +15 -0
- package/src/components/date-time/question.test.js +11 -0
- package/src/components/multi-field-input/question.js +8 -0
- package/src/components/multi-field-input/question.test.js +7 -0
- package/src/questions/question.js +11 -0
- package/src/questions/question.test.js +7 -0
- package/src/validator/address-validator.js +1 -1
- package/src/validator/base-validator.js +1 -1
- package/src/validator/conditional-required-validator.js +1 -1
- package/src/validator/cross-question-validator.README.md +131 -0
- package/src/validator/cross-question-validator.js +48 -5
- package/src/validator/cross-question-validator.test.js +448 -11
- package/src/validator/date-period-validator.js +1 -5
- package/src/validator/date-time-validator.js +1 -5
- package/src/validator/date-validator.js +1 -5
- package/src/validator/required-validator.js +1 -5
- package/src/validator/unit-option-entry-validator.js +1 -5
- package/types/src/components/address/question.d.ts.map +1 -1
- package/types/src/components/date/question.d.ts.map +1 -1
- package/types/src/components/date-period/question.d.ts.map +1 -1
- package/types/src/components/date-time/question.d.ts.map +1 -1
- package/types/src/components/multi-field-input/question.d.ts.map +1 -1
- package/types/src/questions/question.d.ts +8 -0
- package/types/src/questions/question.d.ts.map +1 -1
- package/types/src/validator/address-validator.d.ts +2 -2
- package/types/src/validator/address-validator.d.ts.map +1 -1
- package/types/src/validator/base-validator.d.ts +4 -2
- package/types/src/validator/base-validator.d.ts.map +1 -1
- package/types/src/validator/conditional-required-validator.d.ts +2 -2
- package/types/src/validator/conditional-required-validator.d.ts.map +1 -1
- package/types/src/validator/cross-question-validator.d.ts +11 -4
- package/types/src/validator/cross-question-validator.d.ts.map +1 -1
- package/types/src/validator/date-period-validator.d.ts +2 -6
- package/types/src/validator/date-period-validator.d.ts.map +1 -1
- package/types/src/validator/date-time-validator.d.ts +2 -6
- package/types/src/validator/date-time-validator.d.ts.map +1 -1
- package/types/src/validator/date-validator.d.ts +2 -6
- package/types/src/validator/date-validator.d.ts.map +1 -1
- package/types/src/validator/required-validator.d.ts +2 -6
- package/types/src/validator/required-validator.d.ts.map +1 -1
- package/types/src/validator/unit-option-entry-validator.d.ts +2 -6
- package/types/src/validator/unit-option-entry-validator.d.ts.map +1 -1
|
@@ -14,6 +14,89 @@ function createMockRequest(answers) {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Creates a mock question object with a simple getDataToSave that returns the body field value.
|
|
19
|
+
* @param {string} fieldName
|
|
20
|
+
* @returns {{fieldName: string, bodyFieldNames: string[], getDataToSave: Function}}
|
|
21
|
+
*/
|
|
22
|
+
function createMockQuestion(fieldName) {
|
|
23
|
+
return {
|
|
24
|
+
fieldName,
|
|
25
|
+
bodyFieldNames: [fieldName],
|
|
26
|
+
getDataToSave: async (req) => ({
|
|
27
|
+
answers: {
|
|
28
|
+
[fieldName]: req.body[fieldName]
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a mock date question that extracts date from day/month/year fields.
|
|
36
|
+
* @param {string} fieldName
|
|
37
|
+
* @returns {{fieldName: string, bodyFieldNames: string[], getDataToSave: Function}}
|
|
38
|
+
*/
|
|
39
|
+
function createMockDateQuestion(fieldName) {
|
|
40
|
+
return {
|
|
41
|
+
fieldName,
|
|
42
|
+
bodyFieldNames: [`${fieldName}_day`, `${fieldName}_month`, `${fieldName}_year`],
|
|
43
|
+
getDataToSave: async (req) => {
|
|
44
|
+
const day = req.body[`${fieldName}_day`];
|
|
45
|
+
const month = req.body[`${fieldName}_month`];
|
|
46
|
+
const year = req.body[`${fieldName}_year`];
|
|
47
|
+
if (!day || !month || !year) {
|
|
48
|
+
throw new Error(`Incomplete date fields for ${fieldName}`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
answers: {
|
|
52
|
+
[fieldName]: new Date(year, month - 1, day)
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a mock date-period question that extracts start/end dates.
|
|
61
|
+
* @param {string} fieldName
|
|
62
|
+
* @returns {{fieldName: string, bodyFieldNames: string[], getDataToSave: Function}}
|
|
63
|
+
*/
|
|
64
|
+
function createMockDatePeriodQuestion(fieldName) {
|
|
65
|
+
return {
|
|
66
|
+
fieldName,
|
|
67
|
+
bodyFieldNames: [
|
|
68
|
+
`${fieldName}_start_day`,
|
|
69
|
+
`${fieldName}_start_month`,
|
|
70
|
+
`${fieldName}_start_year`,
|
|
71
|
+
`${fieldName}_end_day`,
|
|
72
|
+
`${fieldName}_end_month`,
|
|
73
|
+
`${fieldName}_end_year`
|
|
74
|
+
],
|
|
75
|
+
getDataToSave: async (req) => {
|
|
76
|
+
const startDay = req.body[`${fieldName}_start_day`];
|
|
77
|
+
const startMonth = req.body[`${fieldName}_start_month`];
|
|
78
|
+
const startYear = req.body[`${fieldName}_start_year`];
|
|
79
|
+
const endDay = req.body[`${fieldName}_end_day`];
|
|
80
|
+
const endMonth = req.body[`${fieldName}_end_month`];
|
|
81
|
+
const endYear = req.body[`${fieldName}_end_year`];
|
|
82
|
+
if (!startDay || !startMonth || !startYear) {
|
|
83
|
+
throw new Error(`Incomplete start date fields for ${fieldName}`);
|
|
84
|
+
}
|
|
85
|
+
if (!endDay || !endMonth || !endYear) {
|
|
86
|
+
throw new Error(`Incomplete end date fields for ${fieldName}`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
answers: {
|
|
90
|
+
[fieldName]: {
|
|
91
|
+
start: new Date(startYear, startMonth - 1, startDay),
|
|
92
|
+
end: new Date(endYear, endMonth - 1, endDay)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
17
100
|
describe('CrossQuestionValidator', () => {
|
|
18
101
|
test('should throw an error if dependencyFieldName is missing', () => {
|
|
19
102
|
assert.throws(() => {
|
|
@@ -39,7 +122,7 @@ describe('CrossQuestionValidator', () => {
|
|
|
39
122
|
dependencyFieldName: 'other',
|
|
40
123
|
validationFunction: validationFn
|
|
41
124
|
});
|
|
42
|
-
const questionObj =
|
|
125
|
+
const questionObj = createMockQuestion('field');
|
|
43
126
|
const req = createMockRequest({ field: 'foo', other: 'bar' });
|
|
44
127
|
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
45
128
|
await chain[0].run(req);
|
|
@@ -51,7 +134,7 @@ describe('CrossQuestionValidator', () => {
|
|
|
51
134
|
dependencyFieldName: 'other',
|
|
52
135
|
validationFunction: () => false
|
|
53
136
|
});
|
|
54
|
-
const questionObj =
|
|
137
|
+
const questionObj = createMockQuestion('field');
|
|
55
138
|
const chain = validator.validate(questionObj);
|
|
56
139
|
const req = createMockRequest({ field: 'foo', other: 'bar' });
|
|
57
140
|
req.body = { field: 'foo' };
|
|
@@ -66,25 +149,28 @@ describe('CrossQuestionValidator', () => {
|
|
|
66
149
|
dependencyFieldName: 'other',
|
|
67
150
|
validationFunction: validationFn
|
|
68
151
|
});
|
|
69
|
-
const questionObj =
|
|
152
|
+
const questionObj = createMockQuestion('field');
|
|
70
153
|
const chain = validator.validate(questionObj);
|
|
71
154
|
const req = { res: { locals: {} } };
|
|
72
155
|
await chain[0].run(req);
|
|
73
156
|
assert.deepEqual(validationFn.mock.calls[0].arguments, [undefined, undefined]);
|
|
74
157
|
});
|
|
75
158
|
|
|
76
|
-
it('should use
|
|
159
|
+
it('should use JourneyResponse answers instead of request body values when useBodyValues is false', async () => {
|
|
77
160
|
const validationFn = mock.fn(() => true);
|
|
78
161
|
const validator = new CrossQuestionValidator({
|
|
79
162
|
dependencyFieldName: 'other',
|
|
80
163
|
validationFunction: validationFn
|
|
81
164
|
});
|
|
82
|
-
const questionObj =
|
|
83
|
-
const req = createMockRequest({ field: 'from-
|
|
165
|
+
const questionObj = createMockQuestion('field');
|
|
166
|
+
const req = createMockRequest({ field: 'from-journey-response', other: 'from-journey-response-dependency' });
|
|
84
167
|
req.body = { field: 'from-body', other: 'from-body-dependency' };
|
|
85
168
|
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
86
169
|
await chain[0].run(req);
|
|
87
|
-
assert.deepEqual(validationFn.mock.calls[0].arguments, [
|
|
170
|
+
assert.deepEqual(validationFn.mock.calls[0].arguments, [
|
|
171
|
+
'from-journey-response',
|
|
172
|
+
'from-journey-response-dependency'
|
|
173
|
+
]);
|
|
88
174
|
});
|
|
89
175
|
|
|
90
176
|
it('should use request body values when useBodyValues is true', async () => {
|
|
@@ -94,9 +180,9 @@ describe('CrossQuestionValidator', () => {
|
|
|
94
180
|
useBodyValues: true,
|
|
95
181
|
validationFunction: validationFn
|
|
96
182
|
});
|
|
97
|
-
const questionObj =
|
|
183
|
+
const questionObj = createMockQuestion('field');
|
|
98
184
|
const chain = validator.validate(questionObj);
|
|
99
|
-
const req = createMockRequest({ field: 'from-
|
|
185
|
+
const req = createMockRequest({ field: 'from-journey-response', other: 'from-journey-response-dependency' });
|
|
100
186
|
req.body = { field: 'from-body', other: 'from-body-dependency' };
|
|
101
187
|
await chain[0].run(req);
|
|
102
188
|
assert.deepEqual(validationFn.mock.calls[0].arguments, ['from-body', 'from-body-dependency']);
|
|
@@ -109,7 +195,7 @@ describe('CrossQuestionValidator', () => {
|
|
|
109
195
|
throw new Error('boom');
|
|
110
196
|
}
|
|
111
197
|
});
|
|
112
|
-
const questionObj =
|
|
198
|
+
const questionObj = createMockQuestion('field');
|
|
113
199
|
const chain = validator.validate(questionObj);
|
|
114
200
|
const req = createMockRequest({ field: 'foo', other: 'bar' });
|
|
115
201
|
const result = await chain[0].run(req);
|
|
@@ -123,10 +209,361 @@ describe('CrossQuestionValidator', () => {
|
|
|
123
209
|
dependencyFieldName: 'other',
|
|
124
210
|
validationFunction: validationFn
|
|
125
211
|
});
|
|
126
|
-
const questionObj =
|
|
212
|
+
const questionObj = createMockQuestion('field');
|
|
127
213
|
const chain = validator.validate(questionObj);
|
|
128
214
|
const req = { res: { locals: { journeyResponse: {} } } };
|
|
129
215
|
await chain[0].run(req);
|
|
130
216
|
assert.deepEqual(validationFn.mock.calls[0].arguments, [undefined, undefined]);
|
|
131
217
|
});
|
|
218
|
+
|
|
219
|
+
describe('date question integration', () => {
|
|
220
|
+
it('should extract date using getDataToSave when useBodyValues is true', async () => {
|
|
221
|
+
const validationFn = mock.fn(() => true);
|
|
222
|
+
const validator = new CrossQuestionValidator({
|
|
223
|
+
dependencyFieldName: 'other',
|
|
224
|
+
validationFunction: validationFn,
|
|
225
|
+
useBodyValues: true
|
|
226
|
+
});
|
|
227
|
+
const questionObj = createMockDateQuestion('dateField');
|
|
228
|
+
const req = createMockRequest({});
|
|
229
|
+
req.body = {
|
|
230
|
+
dateField_day: '15',
|
|
231
|
+
dateField_month: '6',
|
|
232
|
+
dateField_year: '2026',
|
|
233
|
+
other: 'dependency-value'
|
|
234
|
+
};
|
|
235
|
+
const chain = validator.validate(questionObj);
|
|
236
|
+
await chain[0].run(req);
|
|
237
|
+
|
|
238
|
+
const [currentAnswer, dependencyAnswer] = validationFn.mock.calls[0].arguments;
|
|
239
|
+
assert.ok(currentAnswer instanceof Date, 'currentAnswer should be a Date');
|
|
240
|
+
assert.strictEqual(currentAnswer.getFullYear(), 2026);
|
|
241
|
+
assert.strictEqual(currentAnswer.getMonth() + 1, 6);
|
|
242
|
+
assert.strictEqual(currentAnswer.getDate(), 15);
|
|
243
|
+
assert.strictEqual(dependencyAnswer, 'dependency-value');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should propagate error when getDataToSave throws (e.g. incomplete date fields)', async () => {
|
|
247
|
+
const validator = new CrossQuestionValidator({
|
|
248
|
+
dependencyFieldName: 'other',
|
|
249
|
+
validationFunction: () => true,
|
|
250
|
+
useBodyValues: true
|
|
251
|
+
});
|
|
252
|
+
const questionObj = createMockDateQuestion('dateField');
|
|
253
|
+
const req = createMockRequest({});
|
|
254
|
+
req.body = {
|
|
255
|
+
dateField_day: '15',
|
|
256
|
+
dateField_month: '6',
|
|
257
|
+
// missing year
|
|
258
|
+
other: 'dependency-value'
|
|
259
|
+
};
|
|
260
|
+
const chain = validator.validate(questionObj);
|
|
261
|
+
const result = await chain[0].run(req);
|
|
262
|
+
assert.strictEqual(result.isEmpty(), false);
|
|
263
|
+
assert.match(result.array()[0].msg, /Incomplete date fields/);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should use JourneyResponse answer directly when useBodyValues is false', async () => {
|
|
267
|
+
const storedDate = new Date('2026-06-15T00:00:00.000Z');
|
|
268
|
+
const validationFn = mock.fn(() => true);
|
|
269
|
+
const validator = new CrossQuestionValidator({
|
|
270
|
+
dependencyFieldName: 'other',
|
|
271
|
+
validationFunction: validationFn
|
|
272
|
+
});
|
|
273
|
+
const questionObj = createMockDateQuestion('dateField');
|
|
274
|
+
const req = createMockRequest({ dateField: storedDate, other: 'dependency-value' });
|
|
275
|
+
req.body = { dateField_day: '15' };
|
|
276
|
+
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
277
|
+
await chain[0].run(req);
|
|
278
|
+
|
|
279
|
+
const [currentAnswer, dependencyAnswer] = validationFn.mock.calls[0].arguments;
|
|
280
|
+
assert.strictEqual(currentAnswer, storedDate);
|
|
281
|
+
assert.strictEqual(dependencyAnswer, 'dependency-value');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('date-period question integration', () => {
|
|
286
|
+
it('should extract date period using getDataToSave when useBodyValues is true', async () => {
|
|
287
|
+
const validationFn = mock.fn(() => true);
|
|
288
|
+
const validator = new CrossQuestionValidator({
|
|
289
|
+
dependencyFieldName: 'other',
|
|
290
|
+
validationFunction: validationFn,
|
|
291
|
+
useBodyValues: true
|
|
292
|
+
});
|
|
293
|
+
const questionObj = createMockDatePeriodQuestion('periodField');
|
|
294
|
+
const req = createMockRequest({});
|
|
295
|
+
req.body = {
|
|
296
|
+
periodField_start_day: '1',
|
|
297
|
+
periodField_start_month: '6',
|
|
298
|
+
periodField_start_year: '2026',
|
|
299
|
+
periodField_end_day: '30',
|
|
300
|
+
periodField_end_month: '6',
|
|
301
|
+
periodField_end_year: '2026',
|
|
302
|
+
other: 'dependency-value'
|
|
303
|
+
};
|
|
304
|
+
const chain = validator.validate(questionObj);
|
|
305
|
+
await chain[0].run(req);
|
|
306
|
+
|
|
307
|
+
const [currentAnswer, dependencyAnswer] = validationFn.mock.calls[0].arguments;
|
|
308
|
+
assert.ok(currentAnswer.start instanceof Date, 'start should be a Date');
|
|
309
|
+
assert.ok(currentAnswer.end instanceof Date, 'end should be a Date');
|
|
310
|
+
assert.strictEqual(currentAnswer.start.getDate(), 1);
|
|
311
|
+
assert.strictEqual(currentAnswer.end.getDate(), 30);
|
|
312
|
+
assert.strictEqual(dependencyAnswer, 'dependency-value');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should propagate error when getDataToSave throws for incomplete start date', async () => {
|
|
316
|
+
const validator = new CrossQuestionValidator({
|
|
317
|
+
dependencyFieldName: 'other',
|
|
318
|
+
validationFunction: () => true,
|
|
319
|
+
useBodyValues: true
|
|
320
|
+
});
|
|
321
|
+
const questionObj = createMockDatePeriodQuestion('periodField');
|
|
322
|
+
const req = createMockRequest({});
|
|
323
|
+
req.body = {
|
|
324
|
+
periodField_start_day: '1',
|
|
325
|
+
// missing start month and year
|
|
326
|
+
periodField_end_day: '30',
|
|
327
|
+
periodField_end_month: '6',
|
|
328
|
+
periodField_end_year: '2026',
|
|
329
|
+
other: 'dependency-value'
|
|
330
|
+
};
|
|
331
|
+
const chain = validator.validate(questionObj);
|
|
332
|
+
const result = await chain[0].run(req);
|
|
333
|
+
assert.strictEqual(result.isEmpty(), false);
|
|
334
|
+
assert.match(result.array()[0].msg, /Incomplete start date fields/);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should propagate error when getDataToSave throws for incomplete end date', async () => {
|
|
338
|
+
const validator = new CrossQuestionValidator({
|
|
339
|
+
dependencyFieldName: 'other',
|
|
340
|
+
validationFunction: () => true,
|
|
341
|
+
useBodyValues: true
|
|
342
|
+
});
|
|
343
|
+
const questionObj = createMockDatePeriodQuestion('periodField');
|
|
344
|
+
const req = createMockRequest({});
|
|
345
|
+
req.body = {
|
|
346
|
+
periodField_start_day: '1',
|
|
347
|
+
periodField_start_month: '6',
|
|
348
|
+
periodField_start_year: '2026',
|
|
349
|
+
periodField_end_day: '30',
|
|
350
|
+
// missing end month and year
|
|
351
|
+
other: 'dependency-value'
|
|
352
|
+
};
|
|
353
|
+
const chain = validator.validate(questionObj);
|
|
354
|
+
const result = await chain[0].run(req);
|
|
355
|
+
assert.strictEqual(result.isEmpty(), false);
|
|
356
|
+
assert.match(result.array()[0].msg, /Incomplete end date fields/);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should use JourneyResponse answer directly when useBodyValues is false', async () => {
|
|
360
|
+
const storedPeriod = {
|
|
361
|
+
start: new Date('2026-06-01T00:00:00.000Z'),
|
|
362
|
+
end: new Date('2026-06-30T00:00:00.000Z')
|
|
363
|
+
};
|
|
364
|
+
const validationFn = mock.fn(() => true);
|
|
365
|
+
const validator = new CrossQuestionValidator({
|
|
366
|
+
dependencyFieldName: 'other',
|
|
367
|
+
validationFunction: validationFn
|
|
368
|
+
});
|
|
369
|
+
const questionObj = createMockDatePeriodQuestion('periodField');
|
|
370
|
+
const req = createMockRequest({ periodField: storedPeriod, other: 'dependency-value' });
|
|
371
|
+
req.body = { periodField_start_day: '1' };
|
|
372
|
+
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
373
|
+
await chain[0].run(req);
|
|
374
|
+
|
|
375
|
+
const [currentAnswer, dependencyAnswer] = validationFn.mock.calls[0].arguments;
|
|
376
|
+
assert.strictEqual(currentAnswer, storedPeriod);
|
|
377
|
+
assert.strictEqual(dependencyAnswer, 'dependency-value');
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('useBodyValuesForCurrent', () => {
|
|
382
|
+
it('should set useBodyValuesForCurrent property correctly', () => {
|
|
383
|
+
const validator1 = new CrossQuestionValidator({
|
|
384
|
+
dependencyFieldName: 'other',
|
|
385
|
+
validationFunction: () => true,
|
|
386
|
+
useBodyValuesForCurrent: true
|
|
387
|
+
});
|
|
388
|
+
assert.strictEqual(validator1.useBodyValuesForCurrent, true);
|
|
389
|
+
|
|
390
|
+
const validator2 = new CrossQuestionValidator({
|
|
391
|
+
dependencyFieldName: 'other',
|
|
392
|
+
validationFunction: () => true,
|
|
393
|
+
useBodyValues: true
|
|
394
|
+
});
|
|
395
|
+
assert.strictEqual(validator2.useBodyValuesForCurrent, true);
|
|
396
|
+
|
|
397
|
+
const validator3 = new CrossQuestionValidator({
|
|
398
|
+
dependencyFieldName: 'other',
|
|
399
|
+
validationFunction: () => true
|
|
400
|
+
});
|
|
401
|
+
assert.strictEqual(validator3.useBodyValuesForCurrent, false);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should use body value for current and JourneyResponse value for dependency when useBodyValuesForCurrent is true', async () => {
|
|
405
|
+
const validationFn = mock.fn(() => true);
|
|
406
|
+
const validator = new CrossQuestionValidator({
|
|
407
|
+
dependencyFieldName: 'other',
|
|
408
|
+
validationFunction: validationFn,
|
|
409
|
+
useBodyValuesForCurrent: true
|
|
410
|
+
});
|
|
411
|
+
const questionObj = createMockQuestion('field');
|
|
412
|
+
const req = createMockRequest({ field: 'from-journey-response', other: 'from-journey-response-dependency' });
|
|
413
|
+
req.body = { field: 'from-body', other: 'from-body-dependency' };
|
|
414
|
+
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
415
|
+
await chain[0].run(req);
|
|
416
|
+
|
|
417
|
+
const [currentAnswer, dependencyAnswer] = validationFn.mock.calls[0].arguments;
|
|
418
|
+
assert.strictEqual(currentAnswer, 'from-body');
|
|
419
|
+
assert.strictEqual(dependencyAnswer, 'from-journey-response-dependency');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should use body values for both current and dependency when useBodyValues is true', async () => {
|
|
423
|
+
const validationFn = mock.fn(() => true);
|
|
424
|
+
const validator = new CrossQuestionValidator({
|
|
425
|
+
dependencyFieldName: 'other',
|
|
426
|
+
validationFunction: validationFn,
|
|
427
|
+
useBodyValues: true
|
|
428
|
+
});
|
|
429
|
+
const questionObj = createMockQuestion('field');
|
|
430
|
+
const req = createMockRequest({ field: 'from-journey-response', other: 'from-journey-response-dependency' });
|
|
431
|
+
req.body = { field: 'from-body', other: 'from-body-dependency' };
|
|
432
|
+
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
433
|
+
await chain[0].run(req);
|
|
434
|
+
|
|
435
|
+
const [currentAnswer, dependencyAnswer] = validationFn.mock.calls[0].arguments;
|
|
436
|
+
assert.strictEqual(currentAnswer, 'from-body');
|
|
437
|
+
assert.strictEqual(dependencyAnswer, 'from-body-dependency');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should use JourneyResponse value for current when useBodyValuesForCurrent is false', async () => {
|
|
441
|
+
const validationFn = mock.fn(() => true);
|
|
442
|
+
const validator = new CrossQuestionValidator({
|
|
443
|
+
dependencyFieldName: 'other',
|
|
444
|
+
validationFunction: validationFn,
|
|
445
|
+
useBodyValuesForCurrent: false
|
|
446
|
+
});
|
|
447
|
+
const questionObj = createMockQuestion('field');
|
|
448
|
+
const req = createMockRequest({ field: 'from-journey-response', other: 'from-journey-response-dependency' });
|
|
449
|
+
req.body = { field: 'from-body', other: 'from-body-dependency' };
|
|
450
|
+
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
451
|
+
await chain[0].run(req);
|
|
452
|
+
|
|
453
|
+
const [currentAnswer, dependencyAnswer] = validationFn.mock.calls[0].arguments;
|
|
454
|
+
assert.strictEqual(currentAnswer, 'from-journey-response');
|
|
455
|
+
assert.strictEqual(dependencyAnswer, 'from-journey-response-dependency');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should fallback to req.body[fieldName] when getDataToSave is not available', async () => {
|
|
459
|
+
const validationFn = mock.fn(() => true);
|
|
460
|
+
const validator = new CrossQuestionValidator({
|
|
461
|
+
dependencyFieldName: 'other',
|
|
462
|
+
validationFunction: validationFn,
|
|
463
|
+
useBodyValuesForCurrent: true
|
|
464
|
+
});
|
|
465
|
+
// Question without getDataToSave method
|
|
466
|
+
const questionObj = {
|
|
467
|
+
fieldName: 'field'
|
|
468
|
+
};
|
|
469
|
+
const req = createMockRequest({ field: 'from-journey-response', other: 'from-journey-response-dependency' });
|
|
470
|
+
req.body = { field: 'from-body', other: 'from-body-dependency' };
|
|
471
|
+
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
472
|
+
await chain[0].run(req);
|
|
473
|
+
|
|
474
|
+
const [currentAnswer, dependencyAnswer] = validationFn.mock.calls[0].arguments;
|
|
475
|
+
assert.strictEqual(currentAnswer, 'from-body');
|
|
476
|
+
assert.strictEqual(dependencyAnswer, 'from-journey-response-dependency');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should return all formatted answers when fieldName is not in formattedAnswers', async () => {
|
|
480
|
+
const validationFn = mock.fn(() => true);
|
|
481
|
+
const validator = new CrossQuestionValidator({
|
|
482
|
+
dependencyFieldName: 'otherField',
|
|
483
|
+
validationFunction: validationFn,
|
|
484
|
+
useBodyValues: true
|
|
485
|
+
});
|
|
486
|
+
// Mock a behavior where getDataToSave returns answers keyed by input
|
|
487
|
+
// field names, not the question's fieldName
|
|
488
|
+
const questionObj = {
|
|
489
|
+
fieldName: 'contactDetails', // This won't be in formattedAnswers
|
|
490
|
+
bodyFieldNames: ['email', 'phone'],
|
|
491
|
+
getDataToSave: async (req) => ({
|
|
492
|
+
answers: {
|
|
493
|
+
// Returns individual input fields, not 'contactDetails'
|
|
494
|
+
email: req.body.email,
|
|
495
|
+
phone: req.body.phone
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
};
|
|
499
|
+
const req = createMockRequest({});
|
|
500
|
+
req.body = {
|
|
501
|
+
contactDetails: 'ignored',
|
|
502
|
+
email: 'test@example.com',
|
|
503
|
+
phone: '123456789',
|
|
504
|
+
otherField: 'dependency-value'
|
|
505
|
+
};
|
|
506
|
+
const chain = validator.validate(questionObj);
|
|
507
|
+
await chain[0].run(req);
|
|
508
|
+
|
|
509
|
+
const [currentAnswer, dependencyAnswer] = validationFn.mock.calls[0].arguments;
|
|
510
|
+
// Should receive all formatted answers since 'contactDetails' is not in formattedAnswers
|
|
511
|
+
assert.deepStrictEqual(currentAnswer, {
|
|
512
|
+
email: 'test@example.com',
|
|
513
|
+
phone: '123456789'
|
|
514
|
+
});
|
|
515
|
+
assert.strictEqual(dependencyAnswer, 'dependency-value');
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('bodyFieldNames getter support', () => {
|
|
520
|
+
it('should use bodyFieldNames[0] when question provides it', async () => {
|
|
521
|
+
const validationFn = mock.fn(() => true);
|
|
522
|
+
const validator = new CrossQuestionValidator({
|
|
523
|
+
dependencyFieldName: 'other',
|
|
524
|
+
validationFunction: validationFn
|
|
525
|
+
});
|
|
526
|
+
// Mock question with custom bodyFieldNames getter
|
|
527
|
+
const questionObj = {
|
|
528
|
+
fieldName: 'customField',
|
|
529
|
+
bodyFieldNames: ['customField_first', 'customField_second'],
|
|
530
|
+
getDataToSave: async (req) => ({
|
|
531
|
+
answers: {
|
|
532
|
+
customField: req.body.customField_first + '-' + req.body.customField_second
|
|
533
|
+
}
|
|
534
|
+
})
|
|
535
|
+
};
|
|
536
|
+
const req = createMockRequest({ customField: 'stored-value', other: 'dep-value' });
|
|
537
|
+
req.body = {
|
|
538
|
+
customField_first: 'a',
|
|
539
|
+
customField_second: 'b',
|
|
540
|
+
other: 'body-dep'
|
|
541
|
+
};
|
|
542
|
+
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
543
|
+
await chain[0].run(req);
|
|
544
|
+
|
|
545
|
+
// Should have called validation function with stored answer since useBodyValuesForCurrent is false
|
|
546
|
+
assert.deepEqual(validationFn.mock.calls[0].arguments, ['stored-value', 'dep-value']);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('should fallback to fieldName when bodyFieldNames is not available', async () => {
|
|
550
|
+
const validationFn = mock.fn(() => true);
|
|
551
|
+
const validator = new CrossQuestionValidator({
|
|
552
|
+
dependencyFieldName: 'other',
|
|
553
|
+
validationFunction: validationFn
|
|
554
|
+
});
|
|
555
|
+
// Question without bodyFieldNames getter
|
|
556
|
+
const questionObj = {
|
|
557
|
+
fieldName: 'simpleField'
|
|
558
|
+
};
|
|
559
|
+
const req = createMockRequest({ simpleField: 'stored-value', other: 'dep-value' });
|
|
560
|
+
req.body = {
|
|
561
|
+
simpleField: 'body-value'
|
|
562
|
+
};
|
|
563
|
+
const chain = validator.validate(questionObj, req.res.locals.journeyResponse);
|
|
564
|
+
await chain[0].run(req);
|
|
565
|
+
|
|
566
|
+
assert.deepEqual(validationFn.mock.calls[0].arguments, ['stored-value', 'dep-value']);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
132
569
|
});
|
|
@@ -6,10 +6,6 @@ import { enGB } from 'date-fns/locale';
|
|
|
6
6
|
import BaseValidator from './base-validator.js';
|
|
7
7
|
import { parseDateInput, startOfDay } from '../lib/date-utils.js';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {import('../components/date/question.js')} DateQuestion
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
9
|
/**
|
|
14
10
|
* @typedef {Object} DateValidationSettings
|
|
15
11
|
* @property {Boolean} ensureFuture
|
|
@@ -64,7 +60,7 @@ export class DatePeriodValidator extends BaseValidator {
|
|
|
64
60
|
|
|
65
61
|
/**
|
|
66
62
|
* validates the response body, checking the values sent for the date are valid
|
|
67
|
-
* @param {DateQuestion} questionObj
|
|
63
|
+
* @param {import('../components/date/question.js').DateQuestion} questionObj
|
|
68
64
|
*/
|
|
69
65
|
validate(questionObj) {
|
|
70
66
|
const fieldName = questionObj.fieldName;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { body } from 'express-validator';
|
|
2
2
|
import DateValidator from './date-validator.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {import('./question.js')} DateTimeQuestion
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
4
|
/**
|
|
9
5
|
* enforces a user has entered a valid date
|
|
10
6
|
* @class
|
|
@@ -32,7 +28,7 @@ export class DateTimeValidator extends DateValidator {
|
|
|
32
28
|
|
|
33
29
|
/**
|
|
34
30
|
* validates the response body, checking the values sent for the date are valid
|
|
35
|
-
* @param {DateTimeQuestion} questionObj
|
|
31
|
+
* @param {import('../components/date-time/question.js').DateTimeQuestion} questionObj
|
|
36
32
|
*/
|
|
37
33
|
validate(questionObj) {
|
|
38
34
|
const fieldName = questionObj.fieldName;
|
|
@@ -5,10 +5,6 @@ import { enGB } from 'date-fns/locale';
|
|
|
5
5
|
import BaseValidator from './base-validator.js';
|
|
6
6
|
import { endOfDay, parseDateInput, startOfDay } from '../lib/date-utils.js';
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {import('../components/date/question.js')} DateQuestion
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
8
|
/**
|
|
13
9
|
* @typedef {Object} DateValidationSettings
|
|
14
10
|
* @property {Boolean} [ensureFuture]
|
|
@@ -69,7 +65,7 @@ export class DateValidator extends BaseValidator {
|
|
|
69
65
|
|
|
70
66
|
/**
|
|
71
67
|
* validates the response body, checking the values sent for the date are valid
|
|
72
|
-
* @param {DateQuestion} questionObj
|
|
68
|
+
* @param {import('../components/date/question.js').DateQuestion} questionObj
|
|
73
69
|
*/
|
|
74
70
|
validate(questionObj) {
|
|
75
71
|
const fieldName = questionObj.fieldName;
|
|
@@ -2,10 +2,6 @@ import { body } from 'express-validator';
|
|
|
2
2
|
|
|
3
3
|
import BaseValidator from './base-validator.js';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {import('../questions/question.js')} Question
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
5
|
/**
|
|
10
6
|
* enforces a field is not empty
|
|
11
7
|
* @class
|
|
@@ -30,7 +26,7 @@ export class RequiredValidator extends BaseValidator {
|
|
|
30
26
|
|
|
31
27
|
/**
|
|
32
28
|
* validates the response body, checking the questionObj's fieldname
|
|
33
|
-
* @param {Question} questionObj
|
|
29
|
+
* @param {import('../questions/question.js').Question} questionObj
|
|
34
30
|
*/
|
|
35
31
|
validate(questionObj) {
|
|
36
32
|
return body(questionObj.fieldName).notEmpty().withMessage(this.errorMessage);
|
|
@@ -3,10 +3,6 @@ import { body } from 'express-validator';
|
|
|
3
3
|
import BaseValidator from './base-validator.js';
|
|
4
4
|
import { toArray } from '#src/lib/utils.js';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* @typedef {import('../questions/question.js')} Question
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
6
|
/**
|
|
11
7
|
* enforces a field is not empty when condition is satisfied
|
|
12
8
|
* @class
|
|
@@ -34,7 +30,7 @@ export class UnitOptionEntryValidator extends BaseValidator {
|
|
|
34
30
|
|
|
35
31
|
/**
|
|
36
32
|
* validates the response body, checking the questionObj's fieldname
|
|
37
|
-
* @param {
|
|
33
|
+
* @param {import('../components/unit-option-entry/question.js').UnitOptionEntryQuestion} questionObj
|
|
38
34
|
*/
|
|
39
35
|
validate(questionObj) {
|
|
40
36
|
return questionObj.options.reduce((schema, option) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/address/question.js"],"names":[],"mappings":"AAOA;IACC;;OAEG;IACH,oBAFW,OAAO,iBAAiB,EAAE,kBAAkB,EAqBtD;IAXE;;;;;;kBAA8C;IAIhD;;;;;;MAMC;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/address/question.js"],"names":[],"mappings":"AAOA;IACC;;OAEG;IACH,oBAFW,OAAO,iBAAiB,EAAE,kBAAkB,EAqBtD;IAXE;;;;;;kBAA8C;IAIhD;;;;;;MAMC;IAiBF;;;OAGG;IACH,4BAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjB,GAAC,GAAC,MAAM,CAqBpB;IAqCD;;;OAGG;IACH;;eAUC;IAuBD,kEAMC;CACD;;yBAtJwB,WAAW"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/date/question.js"],"names":[],"mappings":"AAKA;;GAEG;AACH;IACC;;;OAGG;IACH,uCAHW,OAAO,iBAAiB,EAAE,kBAAkB,EAStD;IADA,gBAA4B;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/date/question.js"],"names":[],"mappings":"AAKA;;GAEG;AACH;IACC;;;OAGG;IACH,uCAHW,OAAO,iBAAiB,EAAE,kBAAkB,EAStD;IADA,gBAA4B;IAW7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAY5B;IAED;;MAwBC;CAoBD;;yBA1FwB,6BAA6B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/date-period/question.js"],"names":[],"mappings":"AAOA;;;GAGG;AACH;IACC;;;;;;;;;OASG;IACH,6FATW,OAAO,iBAAiB,EAAE,kBAAkB,EAqBtD;IAPA,gBAA4B;IAC5B,YAAsD;IAEtD,eAA0B;IAC1B,aAAsB;IACtB,eAA+D;IAC/D,aAA2D;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/date-period/question.js"],"names":[],"mappings":"AAOA;;;GAGG;AACH;IACC;;;;;;;;;OASG;IACH,6FATW,OAAO,iBAAiB,EAAE,kBAAkB,EAqBtD;IAPA,gBAA4B;IAC5B,YAAsD;IAEtD,eAA0B;IAC1B,aAAsB;IACtB,eAA+D;IAC/D,aAA2D;IAkB5D;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAgC5B;IAED;;MAuCC;CAyCD;;yBAxKwB,WAAW"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/date-time/question.js"],"names":[],"mappings":"AAMA;IACC,kBAAiB;IACjB,kBAAiB;IAEjB;;;;OAIG;IACH,mDAJW,OAAO,iBAAiB,EAAE,kBAAkB,EAWtD;IAFA,gBAA4B;IAC5B,gBAA4B;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../../src/components/date-time/question.js"],"names":[],"mappings":"AAMA;IACC,kBAAiB;IACjB,kBAAiB;IAEjB;;;;OAIG;IACH,mDAJW,OAAO,iBAAiB,EAAE,kBAAkB,EAWtD;IAFA,gBAA4B;IAC5B,gBAA4B;IAkB7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAE,MAAM,CAAC,CAqB5B;IAED;;MAqCC;;CAoCD;;yBA3IwB,6BAA6B"}
|
|
@@ -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,wCAFW,OAAO,oCAAoC,EAAE,4BAA4B,EAanF;IAJC,uEAA8B;
|
|
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,wCAFW,OAAO,oCAAoC,EAAE,4BAA4B,EAanF;IAJC,uEAA8B;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"}
|
|
@@ -111,6 +111,14 @@ export class Question {
|
|
|
111
111
|
* @returns {boolean}
|
|
112
112
|
*/
|
|
113
113
|
get isInManageListSection(): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Gets the body field names used by this question in form submissions.
|
|
116
|
+
* Returns the field names that should be present in req.body when this question is submitted.
|
|
117
|
+
* Subclasses should override this for questions with multiple or differently-named body fields.
|
|
118
|
+
*
|
|
119
|
+
* @returns {string[]}
|
|
120
|
+
*/
|
|
121
|
+
get bodyFieldNames(): string[];
|
|
114
122
|
/**
|
|
115
123
|
* gets the view model for this question
|
|
116
124
|
*
|