@planning-inspectorate/dynamic-forms 3.14.1 → 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/cross-question-validator.README.md +131 -0
- package/src/validator/cross-question-validator.js +47 -4
- package/src/validator/cross-question-validator.test.js +448 -11
- 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/cross-question-validator.d.ts +9 -2
- package/types/src/validator/cross-question-validator.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -30,6 +30,20 @@ export class AddressQuestion extends Question {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Gets the body field names used by this question in form submissions.
|
|
35
|
+
* @returns {string[]}
|
|
36
|
+
*/
|
|
37
|
+
get bodyFieldNames() {
|
|
38
|
+
return [
|
|
39
|
+
`${this.fieldName}_addressLine1`,
|
|
40
|
+
`${this.fieldName}_addressLine2`,
|
|
41
|
+
`${this.fieldName}_townCity`,
|
|
42
|
+
`${this.fieldName}_county`,
|
|
43
|
+
`${this.fieldName}_postcode`
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
/**
|
|
34
48
|
* @param {Record<string, any>} answers
|
|
35
49
|
* @returns {*|string}
|
|
@@ -56,6 +56,19 @@ describe('AddressQuestion', () => {
|
|
|
56
56
|
return { question, mockApi, req, journeyResponse };
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
describe('bodyFieldNames', () => {
|
|
60
|
+
it('should return all address sub-field names', () => {
|
|
61
|
+
const { question } = setup();
|
|
62
|
+
assert.deepStrictEqual(question.bodyFieldNames, [
|
|
63
|
+
`${FIELDNAME}_addressLine1`,
|
|
64
|
+
`${FIELDNAME}_addressLine2`,
|
|
65
|
+
`${FIELDNAME}_townCity`,
|
|
66
|
+
`${FIELDNAME}_county`,
|
|
67
|
+
`${FIELDNAME}_postcode`
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
59
72
|
describe('getDataToSave', () => {
|
|
60
73
|
it('should format the data correctly to be saved', async () => {
|
|
61
74
|
const { question, req } = setup();
|
|
@@ -19,6 +19,14 @@ export class DateQuestion extends Question {
|
|
|
19
19
|
this.dateFormat = dateFormat;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Gets the body field names used by this question in form submissions.
|
|
24
|
+
* @returns {string[]}
|
|
25
|
+
*/
|
|
26
|
+
get bodyFieldNames() {
|
|
27
|
+
return [`${this.fieldName}_day`, `${this.fieldName}_month`, `${this.fieldName}_year`];
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
/**
|
|
23
31
|
* Get the data to save from the request, returns an object of answers
|
|
24
32
|
* @param {import('express').Request} req
|
|
@@ -26,6 +26,23 @@ describe('DateQuestion', () => {
|
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
describe('bodyFieldNames', () => {
|
|
30
|
+
it('should return day, month, and year field names', () => {
|
|
31
|
+
const dateQuestion = new DateQuestion({
|
|
32
|
+
title: TITLE,
|
|
33
|
+
question: QUESTION,
|
|
34
|
+
fieldName: FIELDNAME,
|
|
35
|
+
hint: HINT,
|
|
36
|
+
validators: VALIDATORS
|
|
37
|
+
});
|
|
38
|
+
assert.deepStrictEqual(dateQuestion.bodyFieldNames, [
|
|
39
|
+
`${FIELDNAME}_day`,
|
|
40
|
+
`${FIELDNAME}_month`,
|
|
41
|
+
`${FIELDNAME}_year`
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
29
46
|
describe('getDataToSave', () => {
|
|
30
47
|
it('should return data correctly', async () => {
|
|
31
48
|
const dateQuestion = new DateQuestion({
|
|
@@ -34,6 +34,21 @@ export class DatePeriodQuestion extends Question {
|
|
|
34
34
|
this.endTime = endTime || { hour: 0, minute: 0, second: 0 };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Gets the body field names used by this question in form submissions.
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
get bodyFieldNames() {
|
|
42
|
+
return [
|
|
43
|
+
`${this.fieldName}_start_day`,
|
|
44
|
+
`${this.fieldName}_start_month`,
|
|
45
|
+
`${this.fieldName}_start_year`,
|
|
46
|
+
`${this.fieldName}_end_day`,
|
|
47
|
+
`${this.fieldName}_end_month`,
|
|
48
|
+
`${this.fieldName}_end_year`
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
/**
|
|
38
53
|
* Get the data to save from the request, returns an object of answers
|
|
39
54
|
* @param {import('express').Request} req
|
|
@@ -30,6 +30,26 @@ describe('DatePeriodQuestion', () => {
|
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
describe('bodyFieldNames', () => {
|
|
34
|
+
it('should return all start and end date field names', () => {
|
|
35
|
+
const dateQuestion = new DatePeriodQuestion({
|
|
36
|
+
title: TITLE,
|
|
37
|
+
question: QUESTION,
|
|
38
|
+
fieldName: FIELDNAME,
|
|
39
|
+
hint: HINT,
|
|
40
|
+
validators: VALIDATORS
|
|
41
|
+
});
|
|
42
|
+
assert.deepStrictEqual(dateQuestion.bodyFieldNames, [
|
|
43
|
+
`${FIELDNAME}_start_day`,
|
|
44
|
+
`${FIELDNAME}_start_month`,
|
|
45
|
+
`${FIELDNAME}_start_year`,
|
|
46
|
+
`${FIELDNAME}_end_day`,
|
|
47
|
+
`${FIELDNAME}_end_month`,
|
|
48
|
+
`${FIELDNAME}_end_year`
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
33
53
|
describe('getDataToSave', () => {
|
|
34
54
|
it('should return data correctly', async () => {
|
|
35
55
|
const dateQuestion = new DatePeriodQuestion({
|
|
@@ -22,6 +22,21 @@ export class DateTimeQuestion extends Question {
|
|
|
22
22
|
this.timeFormat = timeFormat;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Gets the body field names used by this question in form submissions.
|
|
27
|
+
* @returns {string[]}
|
|
28
|
+
*/
|
|
29
|
+
get bodyFieldNames() {
|
|
30
|
+
return [
|
|
31
|
+
`${this.fieldName}_day`,
|
|
32
|
+
`${this.fieldName}_month`,
|
|
33
|
+
`${this.fieldName}_year`,
|
|
34
|
+
`${this.fieldName}_hour`,
|
|
35
|
+
`${this.fieldName}_minutes`,
|
|
36
|
+
`${this.fieldName}_period`
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
/**
|
|
26
41
|
* Get the data to save from the request, returns an object of answers
|
|
27
42
|
* @param {import('express').Request} req
|
|
@@ -15,6 +15,17 @@ describe('./lib/forms/custom-components/date-time/question.js', () => {
|
|
|
15
15
|
it('should create', () => {
|
|
16
16
|
assert.strictEqual(question.viewFolder, 'date-time');
|
|
17
17
|
});
|
|
18
|
+
|
|
19
|
+
it('should return all date and time field names from bodyFieldNames', () => {
|
|
20
|
+
assert.deepStrictEqual(question.bodyFieldNames, [
|
|
21
|
+
'siteVisitDate_day',
|
|
22
|
+
'siteVisitDate_month',
|
|
23
|
+
'siteVisitDate_year',
|
|
24
|
+
'siteVisitDate_hour',
|
|
25
|
+
'siteVisitDate_minutes',
|
|
26
|
+
'siteVisitDate_period'
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
18
29
|
});
|
|
19
30
|
describe('prepQuestionForRendering', () => {
|
|
20
31
|
it('should prep question for rendering when value not in payload', () => {
|
|
@@ -30,6 +30,14 @@ export class MultiFieldInputQuestion extends Question {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Gets the body field names used by this question in form submissions.
|
|
35
|
+
* @returns {string[]}
|
|
36
|
+
*/
|
|
37
|
+
get bodyFieldNames() {
|
|
38
|
+
return this.inputFields.map((inputField) => inputField.fieldName);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
answerForViewModel(answers) {
|
|
34
42
|
return this.inputFields.map((inputField) => {
|
|
35
43
|
return {
|
|
@@ -51,6 +51,13 @@ describe('./src/dynamic-forms/components/multi-field-input/question.js', () => {
|
|
|
51
51
|
}, new Error('inputFields are mandatory'));
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
describe('bodyFieldNames', () => {
|
|
55
|
+
it('should return field names from all inputFields', () => {
|
|
56
|
+
const testQuestion = createMultiFieldInputQuestion();
|
|
57
|
+
assert.deepStrictEqual(testQuestion.bodyFieldNames, ['testField1', 'testField2']);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
54
61
|
describe('prepQuestionForRendering', () => {
|
|
55
62
|
it('should call super and set inputFields', () => {
|
|
56
63
|
const question = createMultiFieldInputQuestion();
|
|
@@ -181,6 +181,17 @@ export class Question {
|
|
|
181
181
|
this._isInManageListSection = value;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Gets the body field names used by this question in form submissions.
|
|
186
|
+
* Returns the field names that should be present in req.body when this question is submitted.
|
|
187
|
+
* Subclasses should override this for questions with multiple or differently-named body fields.
|
|
188
|
+
*
|
|
189
|
+
* @returns {string[]}
|
|
190
|
+
*/
|
|
191
|
+
get bodyFieldNames() {
|
|
192
|
+
return [this.fieldName];
|
|
193
|
+
}
|
|
194
|
+
|
|
184
195
|
/**
|
|
185
196
|
* gets the view model for this question
|
|
186
197
|
*
|
|
@@ -135,6 +135,13 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
describe('bodyFieldNames', () => {
|
|
139
|
+
it('should return an array containing the fieldName by default', () => {
|
|
140
|
+
const question = getTestQuestion();
|
|
141
|
+
assert.deepStrictEqual(question.bodyFieldNames, [FIELDNAME]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
138
145
|
describe('prepQuestionForRendering', () => {
|
|
139
146
|
it('should prepQuestionForRendering', () => {
|
|
140
147
|
const question = getTestQuestion();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Cross-question validator
|
|
2
|
+
|
|
3
|
+
The `CrossQuestionValidator` enables validation of a question's answer against another question's answer using a custom validation function. This is useful for scenarios where the validity of one answer depends on another answer.
|
|
4
|
+
|
|
5
|
+
## Constructor options
|
|
6
|
+
|
|
7
|
+
| Parameter | Type | Default | Description |
|
|
8
|
+
| ------------------------- | ---------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `dependencyFieldName` | `string` | **required** | The field name of the other question to compare against |
|
|
10
|
+
| `validationFunction` | `(currentAnswer, dependencyAnswer) => boolean` | **required** | Function that returns `true` if valid, or throws an `Error` with a specific message if invalid |
|
|
11
|
+
| `useBodyValues` | `boolean` | `false` | Use `req.body` values for **both** current and dependency answers |
|
|
12
|
+
| `useBodyValuesForCurrent` | `boolean` | `false` | Use `req.body` value for current answer only (dependency still uses JourneyResponse) |
|
|
13
|
+
|
|
14
|
+
## Use cases
|
|
15
|
+
|
|
16
|
+
### 1. Validation between separate manage list questions (JourneyResponse data)
|
|
17
|
+
|
|
18
|
+
When validating between questions where both answers are already saved to the JourneyResponse (e.g., in a manage list journey), use the default settings.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { CrossQuestionValidator } from '@planning-inspectorate/dynamic-forms';
|
|
22
|
+
|
|
23
|
+
const startDateQuestion = {
|
|
24
|
+
type: COMPONENT_TYPES.DATE,
|
|
25
|
+
fieldName: 'startDate',
|
|
26
|
+
question: 'What is the start date?'
|
|
27
|
+
// ... other config
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const endDateQuestion = {
|
|
31
|
+
type: COMPONENT_TYPES.DATE,
|
|
32
|
+
fieldName: 'endDate',
|
|
33
|
+
question: 'What is the end date?',
|
|
34
|
+
validators: [
|
|
35
|
+
new CrossQuestionValidator({
|
|
36
|
+
dependencyFieldName: 'startDate',
|
|
37
|
+
validationFunction: (endDate, startDate) => {
|
|
38
|
+
if (!endDate || !startDate) return true;
|
|
39
|
+
if (new Date(endDate) < new Date(startDate)) {
|
|
40
|
+
throw new Error('End date must be on or after the start date');
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**When to use:** Both questions are on separate pages and their answers are saved to the JourneyResponse before validation runs.
|
|
50
|
+
|
|
51
|
+
### 2. Validation between regular questions (body data with formatting)
|
|
52
|
+
|
|
53
|
+
When the current question's data needs to be extracted from `req.body` and formatted (e.g., date fields that come as separate day/month/year inputs), use `useBodyValuesForCurrent: true`.
|
|
54
|
+
|
|
55
|
+
The validator will automatically use the question's `getDataToSave()` method to format the data correctly.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const projectEndDateQuestion = {
|
|
59
|
+
type: COMPONENT_TYPES.DATE,
|
|
60
|
+
fieldName: 'projectEndDate',
|
|
61
|
+
question: 'When does the project end?',
|
|
62
|
+
validators: [
|
|
63
|
+
new CrossQuestionValidator({
|
|
64
|
+
dependencyFieldName: 'projectStartDate', // Already saved in JourneyResponse
|
|
65
|
+
useBodyValuesForCurrent: true, // Current answer from body, formatted via getDataToSave()
|
|
66
|
+
validationFunction: (endDate, startDate) => {
|
|
67
|
+
if (!endDate || !startDate) return true;
|
|
68
|
+
if (endDate < new Date(startDate)) {
|
|
69
|
+
throw new Error('Project end date must be on or after the project start date');
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**When to use:** The current question's answer is in `req.body` (not yet saved), but the dependency answer is already saved to the JourneyResponse. Works automatically with date questions, date-period questions, and other question types that have a `getDataToSave()` method.
|
|
79
|
+
|
|
80
|
+
### 3. Validation between fields in a multi-field question (both from body)
|
|
81
|
+
|
|
82
|
+
When validating between multiple fields on the same page (e.g., a `MultiFieldInputQuestion`), both current and reference values come from `req.body`. Use `useBodyValues: true`.
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
const siteAreaQuestion = {
|
|
86
|
+
type: COMPONENT_TYPES.MULTI_FIELD_INPUT,
|
|
87
|
+
fieldName: 'siteArea',
|
|
88
|
+
question: 'What is the area of the site?',
|
|
89
|
+
inputFields: [
|
|
90
|
+
{ fieldName: 'siteAreaHectares', label: 'Site area in hectares (optional)' },
|
|
91
|
+
{ fieldName: 'siteAreaSquareMetres', label: 'Site area in square metres (optional)' }
|
|
92
|
+
],
|
|
93
|
+
validators: [
|
|
94
|
+
// Note: this is a custom validator not provided by default
|
|
95
|
+
new MultiFieldInputValidator({
|
|
96
|
+
fields: [
|
|
97
|
+
{
|
|
98
|
+
fieldName: 'siteAreaHectares',
|
|
99
|
+
validators: [
|
|
100
|
+
new CrossQuestionValidator({
|
|
101
|
+
dependencyFieldName: 'siteAreaSquareMetres',
|
|
102
|
+
useBodyValues: true,
|
|
103
|
+
validationFunction: (hectares, squareMetres) => {
|
|
104
|
+
if (hectares?.trim() && squareMetres?.trim()) {
|
|
105
|
+
throw new Error('Enter the site area in either hectares or square metres, not both');
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
})
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**When to use:** Multiple fields are on the same page and both values need to be read from `req.body` before either is saved.
|
|
119
|
+
|
|
120
|
+
## How data sources are determined
|
|
121
|
+
|
|
122
|
+
| `useBodyValues` | `useBodyValuesForCurrent` | Current Answer Source | Dependency Answer Source |
|
|
123
|
+
| --------------- | ------------------------- | ---------------------------- | ------------------------ |
|
|
124
|
+
| `false` | `false` | JourneyResponse | JourneyResponse |
|
|
125
|
+
| `false` | `true` | Body (via `getDataToSave()`) | JourneyResponse |
|
|
126
|
+
| `true` | (ignored) | Body (via `getDataToSave()`) | Body (raw) |
|
|
127
|
+
|
|
128
|
+
## Notes
|
|
129
|
+
|
|
130
|
+
- When `useBodyValuesForCurrent` is `true`, the validator uses the question's `getDataToSave()` method to format body data. This automatically handles complex field types like dates (`fieldName_day`, `fieldName_month`, `fieldName_year`).
|
|
131
|
+
- The validator binds to the appropriate body field using the question's `bodyFieldNames` getter. For simple questions this returns `[fieldName]`, but complex question types override it (e.g., `DateQuestion` returns `[fieldName_day, fieldName_month, fieldName_year]`). Custom question components can define their own `bodyFieldNames` getter to ensure proper validation binding.
|
|
@@ -11,16 +11,24 @@ export class CrossQuestionValidator extends BaseValidator {
|
|
|
11
11
|
validationFunction;
|
|
12
12
|
/** @type {boolean} */
|
|
13
13
|
useBodyValues;
|
|
14
|
+
/** @type {boolean} */
|
|
15
|
+
useBodyValuesForCurrent;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* @param {Object} params
|
|
17
19
|
* @param {string} params.dependencyFieldName - The field name of the other question to compare against.
|
|
18
20
|
* @param {((currentAnswer: unknown, dependencyAnswer: unknown) => boolean)| null} params.validationFunction - A function that takes the current question's answer and the other question's answer and returns true if valid, false otherwise.
|
|
19
|
-
* @param {boolean} [params.useBodyValues=false] - Whether to use req.body values for validation (default: false). Usually CrossQuestionValidator will be for validating across saved
|
|
21
|
+
* @param {boolean} [params.useBodyValues=false] - Whether to use req.body values for validation (default: false). Usually CrossQuestionValidator will be for validating across saved JourneyResponse answers, but if used between multiple fields on one question, it will need to use the body.
|
|
22
|
+
* @param {boolean} [params.useBodyValuesForCurrent=false] - Whether to use req.body values for the current question's answer (default: false). This may help with questions where data is not saved to the JourneyResponse on POST (e.g. non-multiple entities questions).
|
|
20
23
|
* @throws {Error} If validationFunction is not provided or is not a function.
|
|
21
24
|
* @throws {Error} If dependencyFieldName is not provided
|
|
22
25
|
*/
|
|
23
|
-
constructor({
|
|
26
|
+
constructor({
|
|
27
|
+
dependencyFieldName,
|
|
28
|
+
validationFunction,
|
|
29
|
+
useBodyValues = false,
|
|
30
|
+
useBodyValuesForCurrent = false
|
|
31
|
+
} = {}) {
|
|
24
32
|
super();
|
|
25
33
|
|
|
26
34
|
if (!dependencyFieldName) {
|
|
@@ -33,24 +41,59 @@ export class CrossQuestionValidator extends BaseValidator {
|
|
|
33
41
|
this.dependencyFieldName = dependencyFieldName;
|
|
34
42
|
this.validationFunction = validationFunction;
|
|
35
43
|
this.useBodyValues = useBodyValues;
|
|
44
|
+
this.useBodyValuesForCurrent = useBodyValues || useBodyValuesForCurrent;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gets the body field name to bind validation to.
|
|
49
|
+
* Uses the question's bodyFieldNames getter to determine which body field
|
|
50
|
+
* express-validator binds to for triggering validation.
|
|
51
|
+
* @param {import('../questions/question.js').Question} questionObj
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
#getBodyFieldName(questionObj) {
|
|
55
|
+
// Use the first body field name from the question's bodyFieldNames getter
|
|
56
|
+
// This allows questions to define their own body field names, supporting
|
|
57
|
+
// custom components and complex field structures (e.g. date, date-time, address)
|
|
58
|
+
return questionObj.bodyFieldNames?.[0] ?? questionObj.fieldName;
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
/**
|
|
39
62
|
* Validates response body against individual field validators.
|
|
63
|
+
* Uses the question's getDataToSave method to extract and format body values when useBodyValuesForCurrent is true,
|
|
64
|
+
* allowing generic handling of any question type without needing to know its internal structure.
|
|
40
65
|
* @param {import('../questions/question.js').Question} questionObj - The question object containing the fieldName to validate.
|
|
41
66
|
* @param {import('../journey/journey-response.js').JourneyResponse} [journeyResponse={}] - The current journey response, used to access saved answers for validation.
|
|
42
67
|
* @returns {import('express-validator').ValidationChain[]}
|
|
43
68
|
*/
|
|
44
69
|
validate(questionObj, journeyResponse = {}) {
|
|
45
70
|
const fieldName = questionObj.fieldName;
|
|
71
|
+
const bodyFieldName = this.#getBodyFieldName(questionObj);
|
|
46
72
|
|
|
47
73
|
return [
|
|
48
|
-
body(
|
|
74
|
+
body(bodyFieldName).custom(async (value, { req }) => {
|
|
49
75
|
const answers = journeyResponse?.answers || {};
|
|
50
|
-
|
|
76
|
+
|
|
77
|
+
let currentAnswer;
|
|
78
|
+
if (this.useBodyValuesForCurrent) {
|
|
79
|
+
// Use the question's own getDataToSave to format body data generically
|
|
80
|
+
if (typeof questionObj.getDataToSave === 'function') {
|
|
81
|
+
const { answers: formattedAnswers } = await questionObj.getDataToSave(req, journeyResponse);
|
|
82
|
+
// Use the formatted answer if available, otherwise return all formatted answers
|
|
83
|
+
// (handles MultiFieldInputQuestion which returns answers keyed by input field names)
|
|
84
|
+
currentAnswer = fieldName in formattedAnswers ? formattedAnswers[fieldName] : formattedAnswers;
|
|
85
|
+
} else {
|
|
86
|
+
// Fallback to raw body value if getDataToSave is not available
|
|
87
|
+
currentAnswer = req.body[fieldName];
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
currentAnswer = answers[fieldName];
|
|
91
|
+
}
|
|
92
|
+
|
|
51
93
|
const dependencyAnswer = this.useBodyValues
|
|
52
94
|
? req.body[this.dependencyFieldName]
|
|
53
95
|
: answers[this.dependencyFieldName];
|
|
96
|
+
|
|
54
97
|
return (
|
|
55
98
|
this.validationFunction(currentAnswer, dependencyAnswer) ||
|
|
56
99
|
// Fallback if validation fails without throwing an error
|
|
@@ -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
|
});
|
|
@@ -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
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../src/questions/question.js"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["../../../src/questions/question.js"],"names":[],"mappings":"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"}
|
|
@@ -6,14 +6,16 @@ export class CrossQuestionValidator extends BaseValidator {
|
|
|
6
6
|
* @param {Object} params
|
|
7
7
|
* @param {string} params.dependencyFieldName - The field name of the other question to compare against.
|
|
8
8
|
* @param {((currentAnswer: unknown, dependencyAnswer: unknown) => boolean)| null} params.validationFunction - A function that takes the current question's answer and the other question's answer and returns true if valid, false otherwise.
|
|
9
|
-
* @param {boolean} [params.useBodyValues=false] - Whether to use req.body values for validation (default: false). Usually CrossQuestionValidator will be for validating across saved
|
|
9
|
+
* @param {boolean} [params.useBodyValues=false] - Whether to use req.body values for validation (default: false). Usually CrossQuestionValidator will be for validating across saved JourneyResponse answers, but if used between multiple fields on one question, it will need to use the body.
|
|
10
|
+
* @param {boolean} [params.useBodyValuesForCurrent=false] - Whether to use req.body values for the current question's answer (default: false). This may help with questions where data is not saved to the JourneyResponse on POST (e.g. non-multiple entities questions).
|
|
10
11
|
* @throws {Error} If validationFunction is not provided or is not a function.
|
|
11
12
|
* @throws {Error} If dependencyFieldName is not provided
|
|
12
13
|
*/
|
|
13
|
-
constructor({ dependencyFieldName, validationFunction, useBodyValues }?: {
|
|
14
|
+
constructor({ dependencyFieldName, validationFunction, useBodyValues, useBodyValuesForCurrent }?: {
|
|
14
15
|
dependencyFieldName: string;
|
|
15
16
|
validationFunction: ((currentAnswer: unknown, dependencyAnswer: unknown) => boolean) | null;
|
|
16
17
|
useBodyValues?: boolean | undefined;
|
|
18
|
+
useBodyValuesForCurrent?: boolean | undefined;
|
|
17
19
|
});
|
|
18
20
|
/** @type {string} */
|
|
19
21
|
dependencyFieldName: string;
|
|
@@ -21,13 +23,18 @@ export class CrossQuestionValidator extends BaseValidator {
|
|
|
21
23
|
validationFunction: ((currentAnswer: unknown, dependencyAnswer: unknown) => boolean);
|
|
22
24
|
/** @type {boolean} */
|
|
23
25
|
useBodyValues: boolean;
|
|
26
|
+
/** @type {boolean} */
|
|
27
|
+
useBodyValuesForCurrent: boolean;
|
|
24
28
|
/**
|
|
25
29
|
* Validates response body against individual field validators.
|
|
30
|
+
* Uses the question's getDataToSave method to extract and format body values when useBodyValuesForCurrent is true,
|
|
31
|
+
* allowing generic handling of any question type without needing to know its internal structure.
|
|
26
32
|
* @param {import('../questions/question.js').Question} questionObj - The question object containing the fieldName to validate.
|
|
27
33
|
* @param {import('../journey/journey-response.js').JourneyResponse} [journeyResponse={}] - The current journey response, used to access saved answers for validation.
|
|
28
34
|
* @returns {import('express-validator').ValidationChain[]}
|
|
29
35
|
*/
|
|
30
36
|
validate(questionObj: import("../questions/question.js").Question, journeyResponse?: import("../journey/journey-response.js").JourneyResponse): import("express-validator").ValidationChain[];
|
|
37
|
+
#private;
|
|
31
38
|
}
|
|
32
39
|
export default CrossQuestionValidator;
|
|
33
40
|
import BaseValidator from './base-validator.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cross-question-validator.d.ts","sourceRoot":"","sources":["../../../src/validator/cross-question-validator.js"],"names":[],"mappings":"AAGA;;GAEG;AACH;
|
|
1
|
+
{"version":3,"file":"cross-question-validator.d.ts","sourceRoot":"","sources":["../../../src/validator/cross-question-validator.js"],"names":[],"mappings":"AAGA;;GAEG;AACH;IAUC;;;;;;;;OAQG;IACH,kGAPG;QAAuB,mBAAmB,EAAlC,MAAM;QACyE,kBAAkB,EAAjG,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,GAAE,IAAI;QACrD,aAAa;QACb,uBAAuB;KAChD,EAsBF;IArCD,qBAAqB;IACrB,qBADW,MAAM,CACG;IACpB,+EAA+E;IAC/E,oBADW,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,CACxD;IACnB,sBAAsB;IACtB,eADW,OAAO,CACJ;IACd,sBAAsB;IACtB,yBADW,OAAO,CACM;IA8CxB;;;;;;;OAOG;IACH,sBAJW,OAAO,0BAA0B,EAAE,QAAQ,oBAC3C,OAAO,gCAAgC,EAAE,eAAe,GACtD,OAAO,mBAAmB,EAAE,eAAe,EAAE,CAuCzD;;CACD;;0BA1GyB,qBAAqB"}
|