@planning-inspectorate/dynamic-forms 3.20.4 → 3.21.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/.azure/pipelines/azure-pipelines-pr.yml +6 -6
- package/.azure/pipelines/release.yml +3 -3
- package/README.md +49 -0
- package/docs/Getting Started.md +8 -2
- package/package.json +1 -1
- package/src/components/boolean/question.d.ts +21 -2
- package/src/components/boolean/question.d.ts.map +1 -1
- package/src/components/boolean/question.js +46 -1
- package/src/components/boolean/question.test.js +22 -1
- package/src/components/date/question.d.ts +1 -1
- package/src/components/date/question.js +1 -1
- package/src/components/date-period/question.d.ts +1 -1
- package/src/components/date-period/question.js +1 -1
- package/src/components/date-time/question.d.ts +1 -1
- package/src/components/date-time/question.js +1 -1
- package/src/components/multi-field-input/question.js +1 -1
- package/src/components/single-line-input/index.njk +4 -0
- package/src/components/single-line-input/question.d.ts +2 -0
- package/src/components/single-line-input/question.d.ts.map +1 -1
- package/src/components/single-line-input/question.js +4 -0
- package/src/components/single-line-input/question.test.js +6 -0
- package/src/components/unit-option-entry/question.d.ts +1 -1
- package/src/components/unit-option-entry/question.js +1 -1
- package/src/controller.test.js +207 -208
- package/src/questions/options-question.js +1 -1
- package/src/validator/email-validator.d.ts.map +1 -1
- package/src/validator/email-validator.js +4 -0
- package/typedefs/question-props.d.ts +3 -1
|
@@ -35,7 +35,7 @@ extends:
|
|
|
35
35
|
displayName: Cache NPM
|
|
36
36
|
- template: ../steps/node_script.yml
|
|
37
37
|
parameters:
|
|
38
|
-
nodeVersion:
|
|
38
|
+
nodeVersion: 24
|
|
39
39
|
script: npm ci
|
|
40
40
|
environmentVariables:
|
|
41
41
|
npm_config_cache: $(Pipeline.Workspace)/.npm
|
|
@@ -43,21 +43,21 @@ extends:
|
|
|
43
43
|
parameters:
|
|
44
44
|
# only run commitlint on branches, not main
|
|
45
45
|
condition: ne(variables['Build.SourceBranchName'], 'main')
|
|
46
|
-
nodeVersion:
|
|
46
|
+
nodeVersion: 24
|
|
47
47
|
script: npm run commitlint
|
|
48
48
|
- template: ../steps/node_script.yml
|
|
49
49
|
parameters:
|
|
50
|
-
nodeVersion:
|
|
50
|
+
nodeVersion: 24
|
|
51
51
|
script: npm run lint
|
|
52
52
|
- template: ../steps/node_script.yml
|
|
53
53
|
parameters:
|
|
54
|
-
nodeVersion:
|
|
54
|
+
nodeVersion: 24
|
|
55
55
|
script: npm run format-check
|
|
56
56
|
- template: ../steps/node_script.yml
|
|
57
57
|
parameters:
|
|
58
|
-
nodeVersion:
|
|
58
|
+
nodeVersion: 24
|
|
59
59
|
script: npm run test-coverage
|
|
60
60
|
- template: ../steps/node_script.yml
|
|
61
61
|
parameters:
|
|
62
|
-
nodeVersion:
|
|
62
|
+
nodeVersion: 24
|
|
63
63
|
script: npm run generate-types
|
|
@@ -44,15 +44,15 @@ jobs:
|
|
|
44
44
|
- checkout: self
|
|
45
45
|
- template: steps/node_script.yml@templates
|
|
46
46
|
parameters:
|
|
47
|
-
nodeVersion:
|
|
47
|
+
nodeVersion: 24
|
|
48
48
|
script: npm ci
|
|
49
49
|
- template: steps/node_script.yml@templates
|
|
50
50
|
parameters:
|
|
51
|
-
nodeVersion:
|
|
51
|
+
nodeVersion: 24
|
|
52
52
|
script: npm run generate-types
|
|
53
53
|
- template: steps/node_script.yml@templates
|
|
54
54
|
parameters:
|
|
55
|
-
nodeVersion:
|
|
55
|
+
nodeVersion: 24
|
|
56
56
|
script: npx semantic-release --branches main ${{ variables.dryRunFlag }}
|
|
57
57
|
environmentVariables:
|
|
58
58
|
GH_TOKEN: $(GITHUB_TOKEN)
|
package/README.md
CHANGED
|
@@ -82,6 +82,55 @@ The controllers available are:
|
|
|
82
82
|
| buildSave | Returns a controller to handle saving the answer and either redirecting to the next question or show any validation errors if present. Accepts a save function. |
|
|
83
83
|
| list/buildList | Renders a check-your-answers page showing all the answers, links to change any answer, and a button to submit the answers. |
|
|
84
84
|
|
|
85
|
+
#### Saving answers
|
|
86
|
+
|
|
87
|
+
The `buildSave` controller accepts a `SaveDataFn` which is passed the context required to save the answer(s). To save answers to session during a journey, a `SaveDataFn` implementation is provided called `saveDataToSession`.
|
|
88
|
+
|
|
89
|
+
For other use cases, write a custom `SaveDataFn`. A basic implementation (if you are not using ManageList) needs to save the answers passed in as `data.answers`, which is an object whose keys are the question field names, and the values are the answers. For example:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const update: SaveDataFn = async ({data}) => {
|
|
93
|
+
const answers = data.answers;
|
|
94
|
+
|
|
95
|
+
console.log('saving', answers);
|
|
96
|
+
|
|
97
|
+
// TODO: map to database
|
|
98
|
+
// TODO: write to database
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To support `ManageList` questions as well, handle the `isManageListItem`, `manageListQuestionFieldName`, `manageListItemRemove` parameters. For example:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const update: SaveDataFn = async ({data, req, isManageListItem, manageListQuestionFieldName, manageListItemRemove}) => {
|
|
106
|
+
if (isManageListItem) {
|
|
107
|
+
const update = {
|
|
108
|
+
id: req.params.manageListItemId,
|
|
109
|
+
...data.answers
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
console.log('saving', update);
|
|
113
|
+
// TODO: map to database
|
|
114
|
+
// TODO: write to database
|
|
115
|
+
return;
|
|
116
|
+
} else if (manageListItemRemove) {
|
|
117
|
+
const id = req.params.manageListItemId;
|
|
118
|
+
// identify which type of item it is via the field name
|
|
119
|
+
const type = manageListQuestionFieldName;
|
|
120
|
+
console.log('removing item', {id, type});
|
|
121
|
+
// TODO: delete from database
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const answers = data.answers;
|
|
125
|
+
|
|
126
|
+
console.log('saving', answers);
|
|
127
|
+
|
|
128
|
+
// TODO: map to database
|
|
129
|
+
// TODO: write to database
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
|
|
85
134
|
### Conditions
|
|
86
135
|
|
|
87
136
|
Conditions are used to show/hide questions based on the answers to other questions. There are fluent methods on a Section
|
package/docs/Getting Started.md
CHANGED
|
@@ -78,7 +78,7 @@ Dynamic forms requires two templates to work for rendering questions and renderi
|
|
|
78
78
|
### Journey (or question) template
|
|
79
79
|
|
|
80
80
|
The journey template is used for question pages. It must include a block called `dynQuestionContent`, and a block called `head` (which the default gov.uk template has).
|
|
81
|
-
It should also include the rendering of the errorSummary component. The path to this template is configured via the `journeyTemplate` property on a Journey.
|
|
81
|
+
It should also include the rendering of the errorSummary component, and optionally the back link. The path to this template is configured via the `journeyTemplate` property on a Journey.
|
|
82
82
|
|
|
83
83
|
For example:
|
|
84
84
|
|
|
@@ -87,7 +87,13 @@ e.g.
|
|
|
87
87
|
```nunjucks
|
|
88
88
|
{% extends "govuk/template.njk" %}
|
|
89
89
|
|
|
90
|
-
{%
|
|
90
|
+
{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
|
|
91
|
+
{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
|
|
92
|
+
|
|
93
|
+
{% block containerStart %}
|
|
94
|
+
{% if backLink %}
|
|
95
|
+
{{ govukBackLink({ href: backLink }) }}
|
|
96
|
+
{% endif %}
|
|
91
97
|
{% if errorSummary %}
|
|
92
98
|
<div class="govuk-grid-row">
|
|
93
99
|
<div class="govuk-grid-column-two-thirds">
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {"yes"|"no"} YesNo
|
|
3
|
+
*/
|
|
1
4
|
export const BOOLEAN_OPTIONS: Readonly<{
|
|
2
5
|
YES: "yes";
|
|
3
6
|
NO: "no";
|
|
4
7
|
}>;
|
|
5
8
|
export function yesNoToBoolean(value: string | any): boolean;
|
|
6
|
-
export function booleanToYesNoValue(value: any):
|
|
9
|
+
export function booleanToYesNoValue(value: any): YesNo;
|
|
7
10
|
export function booleanToYesNoOrNull(value: boolean | null): string | null;
|
|
8
11
|
export class BooleanQuestion extends RadioQuestion {
|
|
9
12
|
/**
|
|
@@ -14,12 +17,28 @@ export class BooleanQuestion extends RadioQuestion {
|
|
|
14
17
|
/**
|
|
15
18
|
* Get the data to save from the request, returns an object of answers
|
|
16
19
|
* @param {import('express').Request} req
|
|
17
|
-
* @param {JourneyResponse} journeyResponse - current journey response
|
|
20
|
+
* @param {JourneyResponse} journeyResponse - current journey response
|
|
18
21
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
19
22
|
*/ getDataToSave(req: import("express").Request, journeyResponse: JourneyResponse): Promise<{
|
|
20
23
|
answers: Record<string, unknown>;
|
|
21
24
|
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Normalises a stored boolean answer back to 'yes'/'no' before computing the
|
|
27
|
+
* view model value, so the radio's `checked` state resolves correctly.
|
|
28
|
+
*
|
|
29
|
+
* getDataToSave() saves a real boolean. Top-level questions get this converted
|
|
30
|
+
* back to 'yes'/'no' by buildGetJourneyResponseFromSession, but that conversion
|
|
31
|
+
* is shallow and doesn't reach into manage-list item arrays, so nested
|
|
32
|
+
* BooleanQuestions (e.g. a manage-list sub-question) would otherwise receive
|
|
33
|
+
* the raw boolean.
|
|
34
|
+
*
|
|
35
|
+
* @param {Record<string, unknown>} answers
|
|
36
|
+
* @param {boolean} isPayload
|
|
37
|
+
* @returns {unknown|YesNo}
|
|
38
|
+
*/
|
|
39
|
+
answerForViewModel(answers: Record<string, unknown>, isPayload: boolean): unknown | YesNo;
|
|
22
40
|
}
|
|
23
41
|
export default BooleanQuestion;
|
|
42
|
+
export type YesNo = "yes" | "no";
|
|
24
43
|
import RadioQuestion from '../radio/question.js';
|
|
25
44
|
//# sourceMappingURL=question.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;;GAGG;AAMI,sCAHI,MAAM,GAAC,GAAC,GACN,OAAO,CAOnB;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;GAEG;AAEH;;;GAGG;AAMI,sCAHI,MAAM,GAAC,GAAC,GACN,OAAO,CAOnB;AAMM,iDAFK,KAAK,CAIhB;AAMM,4CAHI,OAAO,GAAC,IAAI,GACV,MAAM,GAAC,IAAI,CAOvB;AAED;IACC;;OAEG;IACH,6IAFW,OAAO,+BAA+B,EAAE,qBAAqB,EAmDvE;IADA,oCAAkC;IAGnC;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAezD;IAED;;;;;;;;;;;;;OAaG;IACH,4BAJW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,aACvB,OAAO,GACL,OAAO,GAAC,KAAK,CAQzB;CAgBD;;oBAtJY,KAAK,GAAC,IAAI;0BAHG,sBAAsB"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import RadioQuestion from '../radio/question.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {"yes"|"no"} YesNo
|
|
5
|
+
*/
|
|
6
|
+
|
|
3
7
|
export const BOOLEAN_OPTIONS = Object.freeze({
|
|
4
8
|
YES: 'yes',
|
|
5
9
|
NO: 'no'
|
|
@@ -16,6 +20,10 @@ export const yesNoToBoolean = (value) => {
|
|
|
16
20
|
return value === BOOLEAN_OPTIONS.YES;
|
|
17
21
|
};
|
|
18
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @param value
|
|
25
|
+
* @return {YesNo}
|
|
26
|
+
*/
|
|
19
27
|
export const booleanToYesNoValue = (value) => {
|
|
20
28
|
return value ? BOOLEAN_OPTIONS.YES : BOOLEAN_OPTIONS.NO;
|
|
21
29
|
};
|
|
@@ -89,7 +97,7 @@ export class BooleanQuestion extends RadioQuestion {
|
|
|
89
97
|
/**
|
|
90
98
|
* Get the data to save from the request, returns an object of answers
|
|
91
99
|
* @param {import('express').Request} req
|
|
92
|
-
* @param {JourneyResponse} journeyResponse - current journey response
|
|
100
|
+
* @param {JourneyResponse} journeyResponse - current journey response
|
|
93
101
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
94
102
|
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
95
103
|
async getDataToSave(req, journeyResponse) {
|
|
@@ -106,6 +114,43 @@ export class BooleanQuestion extends RadioQuestion {
|
|
|
106
114
|
|
|
107
115
|
return { answers };
|
|
108
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Normalises a stored boolean answer back to 'yes'/'no' before computing the
|
|
120
|
+
* view model value, so the radio's `checked` state resolves correctly.
|
|
121
|
+
*
|
|
122
|
+
* getDataToSave() saves a real boolean. Top-level questions get this converted
|
|
123
|
+
* back to 'yes'/'no' by buildGetJourneyResponseFromSession, but that conversion
|
|
124
|
+
* is shallow and doesn't reach into manage-list item arrays, so nested
|
|
125
|
+
* BooleanQuestions (e.g. a manage-list sub-question) would otherwise receive
|
|
126
|
+
* the raw boolean.
|
|
127
|
+
*
|
|
128
|
+
* @param {Record<string, unknown>} answers
|
|
129
|
+
* @param {boolean} isPayload
|
|
130
|
+
* @returns {unknown|YesNo}
|
|
131
|
+
*/
|
|
132
|
+
answerForViewModel(answers, isPayload) {
|
|
133
|
+
const rawValue = answers[this.fieldName];
|
|
134
|
+
if (typeof rawValue === 'boolean') {
|
|
135
|
+
return booleanToYesNoValue(rawValue);
|
|
136
|
+
}
|
|
137
|
+
return super.answerForViewModel(answers, isPayload);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Normalises a stored boolean answer back to 'yes'/'no' before formatting,
|
|
142
|
+
* so summary/check-your-answers pages display 'Yes'/'No' instead of the
|
|
143
|
+
* literal 'true'/'false' - see answerForViewModel for why this is needed.
|
|
144
|
+
*
|
|
145
|
+
* @param {unknown} answer
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
formatAnswer(answer) {
|
|
149
|
+
if (typeof answer === 'boolean') {
|
|
150
|
+
return super.formatAnswer(booleanToYesNoValue(answer));
|
|
151
|
+
}
|
|
152
|
+
return super.formatAnswer(answer);
|
|
153
|
+
}
|
|
109
154
|
}
|
|
110
155
|
|
|
111
156
|
export default BooleanQuestion;
|
|
@@ -54,7 +54,9 @@ describe('./src/dynamic-forms/components/boolean/question.js', () => {
|
|
|
54
54
|
const renderTests = [
|
|
55
55
|
{ answers: {}, yesChecked: false, noChecked: false },
|
|
56
56
|
{ answers: { [FIELDNAME]: BOOLEAN_OPTIONS.YES }, yesChecked: true, noChecked: false },
|
|
57
|
-
{ answers: { [FIELDNAME]: BOOLEAN_OPTIONS.NO }, yesChecked: false, noChecked: true }
|
|
57
|
+
{ answers: { [FIELDNAME]: BOOLEAN_OPTIONS.NO }, yesChecked: false, noChecked: true },
|
|
58
|
+
{ answers: { [FIELDNAME]: true }, yesChecked: true, noChecked: false },
|
|
59
|
+
{ answers: { [FIELDNAME]: false }, yesChecked: false, noChecked: true }
|
|
58
60
|
];
|
|
59
61
|
for (const t of renderTests) {
|
|
60
62
|
it(`should render with ${JSON.stringify(t.answers)}`, async () => {
|
|
@@ -90,4 +92,23 @@ describe('./src/dynamic-forms/components/boolean/question.js', () => {
|
|
|
90
92
|
assert.strictEqual(booleanToYesNoValue(t.req), t.expect);
|
|
91
93
|
});
|
|
92
94
|
}
|
|
95
|
+
|
|
96
|
+
describe('formatAnswer', () => {
|
|
97
|
+
it('formats a true boolean answer as Yes', () => {
|
|
98
|
+
const booleanQuestion = newQuestion();
|
|
99
|
+
assert.strictEqual(booleanQuestion.formatAnswer(true), 'Yes');
|
|
100
|
+
});
|
|
101
|
+
it('formats a false boolean answer as No', () => {
|
|
102
|
+
const booleanQuestion = newQuestion();
|
|
103
|
+
assert.strictEqual(booleanQuestion.formatAnswer(false), 'No');
|
|
104
|
+
});
|
|
105
|
+
it('formats a yes string answer as Yes', () => {
|
|
106
|
+
const booleanQuestion = newQuestion();
|
|
107
|
+
assert.strictEqual(booleanQuestion.formatAnswer(BOOLEAN_OPTIONS.YES), 'Yes');
|
|
108
|
+
});
|
|
109
|
+
it('formats a no string answer as No', () => {
|
|
110
|
+
const booleanQuestion = newQuestion();
|
|
111
|
+
assert.strictEqual(booleanQuestion.formatAnswer(BOOLEAN_OPTIONS.NO), 'No');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
93
114
|
});
|
|
@@ -10,7 +10,7 @@ export class DateQuestion extends Question {
|
|
|
10
10
|
/**
|
|
11
11
|
* Get the data to save from the request, returns an object of answers
|
|
12
12
|
* @param {import('express').Request} req
|
|
13
|
-
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
13
|
+
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
14
14
|
* @returns {Promise.<Object>}
|
|
15
15
|
*/ getDataToSave(req: import("express").Request, journeyResponse: import("#journey-response").JourneyResponse): Promise<Object>;
|
|
16
16
|
answerForViewModel(answers: any, isPayload: any): {
|
|
@@ -29,7 +29,7 @@ export class DateQuestion extends Question {
|
|
|
29
29
|
/**
|
|
30
30
|
* Get the data to save from the request, returns an object of answers
|
|
31
31
|
* @param {import('express').Request} req
|
|
32
|
-
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
32
|
+
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
33
33
|
* @returns {Promise.<Object>}
|
|
34
34
|
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
35
35
|
async getDataToSave(req, journeyResponse) {
|
|
@@ -19,7 +19,7 @@ export class DatePeriodQuestion extends Question {
|
|
|
19
19
|
/**
|
|
20
20
|
* Get the data to save from the request, returns an object of answers
|
|
21
21
|
* @param {import('express').Request} req
|
|
22
|
-
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
22
|
+
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
23
23
|
* @returns {Promise.<Object>}
|
|
24
24
|
*/ getDataToSave(req: import("express").Request, journeyResponse: import("#journey-response").JourneyResponse): Promise<Object>;
|
|
25
25
|
answerForViewModel(answers: any, isPayload: any): {
|
|
@@ -54,7 +54,7 @@ export class DatePeriodQuestion extends Question {
|
|
|
54
54
|
/**
|
|
55
55
|
* Get the data to save from the request, returns an object of answers
|
|
56
56
|
* @param {import('express').Request} req
|
|
57
|
-
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
57
|
+
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
58
58
|
* @returns {Promise.<Object>}
|
|
59
59
|
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
60
60
|
async getDataToSave(req, journeyResponse) {
|
|
@@ -10,7 +10,7 @@ export class DateTimeQuestion extends Question {
|
|
|
10
10
|
/**
|
|
11
11
|
* Get the data to save from the request, returns an object of answers
|
|
12
12
|
* @param {import('express').Request} req
|
|
13
|
-
* @param {JourneyResponse} journeyResponse - current journey response
|
|
13
|
+
* @param {JourneyResponse} journeyResponse - current journey response
|
|
14
14
|
* @returns {Promise.<Object>}
|
|
15
15
|
*/ getDataToSave(req: import("express").Request, journeyResponse: JourneyResponse): Promise<Object>;
|
|
16
16
|
answerForViewModel(answers: any, isPayload: any): {
|
|
@@ -38,7 +38,7 @@ export class DateTimeQuestion extends Question {
|
|
|
38
38
|
/**
|
|
39
39
|
* Get the data to save from the request, returns an object of answers
|
|
40
40
|
* @param {import('express').Request} req
|
|
41
|
-
* @param {JourneyResponse} journeyResponse - current journey response
|
|
41
|
+
* @param {JourneyResponse} journeyResponse - current journey response
|
|
42
42
|
* @returns {Promise.<Object>}
|
|
43
43
|
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
44
44
|
async getDataToSave(req, journeyResponse) {
|
|
@@ -44,7 +44,7 @@ export class MultiFieldInputQuestion extends Question {
|
|
|
44
44
|
/**
|
|
45
45
|
* Get the data to save from the request, returns an object of answers
|
|
46
46
|
* @param {import('express').Request} req
|
|
47
|
-
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
47
|
+
* @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
|
|
48
48
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
49
49
|
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
50
50
|
async getDataToSave(req, journeyResponse) {
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
text: errors[question.fieldName].msg
|
|
39
39
|
},
|
|
40
40
|
classes: question.classes,
|
|
41
|
+
prefix: question.prefix,
|
|
42
|
+
suffix: question.suffix,
|
|
41
43
|
attributes: question.attributes,
|
|
42
44
|
type: question.type,
|
|
43
45
|
autocomplete: question.autocomplete
|
|
@@ -62,6 +64,8 @@
|
|
|
62
64
|
text: errors[question.fieldName].msg
|
|
63
65
|
},
|
|
64
66
|
classes: question.classes,
|
|
67
|
+
prefix: question.prefix,
|
|
68
|
+
suffix: question.suffix,
|
|
65
69
|
attributes: question.attributes,
|
|
66
70
|
type: question.type,
|
|
67
71
|
autocomplete: question.autocomplete
|
|
@@ -10,6 +10,8 @@ export class SingleLineInputQuestion extends Question {
|
|
|
10
10
|
inputAttributes: Record<string, string>;
|
|
11
11
|
label: string | undefined;
|
|
12
12
|
classes: string;
|
|
13
|
+
prefix: import("#typedefs/question-props.d.ts").Affix | undefined;
|
|
14
|
+
suffix: import("#typedefs/question-props.d.ts").Affix | undefined;
|
|
13
15
|
}
|
|
14
16
|
export default SingleLineInputQuestion;
|
|
15
17
|
import { Question } from '#question';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;GAEG;AACH;IAIC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,6BAA6B,
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;GAEG;AACH;IAIC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAa/E;IAjBD,qCAAqC;IACrC,iBADW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CACjB;IAWf,0BAAyB;IAEzB,gBAAmC;IACnC,kEAA2B;IAC3B,kEAA2B;CAiB5B;;yBAvCwB,WAAW"}
|
|
@@ -19,6 +19,8 @@ export class SingleLineInputQuestion extends Question {
|
|
|
19
19
|
this.label = params.label;
|
|
20
20
|
this.inputAttributes = params.inputAttributes || {};
|
|
21
21
|
this.classes = params.classes || '';
|
|
22
|
+
this.prefix = params.prefix;
|
|
23
|
+
this.suffix = params.suffix;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -32,6 +34,8 @@ export class SingleLineInputQuestion extends Question {
|
|
|
32
34
|
viewModel.question.type = type;
|
|
33
35
|
|
|
34
36
|
viewModel.question.classes = this.classes;
|
|
37
|
+
viewModel.question.prefix = this.prefix;
|
|
38
|
+
viewModel.question.suffix = this.suffix;
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -11,6 +11,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
|
|
|
11
11
|
const HTML = '/path/to/html.njk';
|
|
12
12
|
const HINT = 'hint';
|
|
13
13
|
const LABEL = 'A label';
|
|
14
|
+
const PREFIX = { text: '£', classes: 'govuk-!-font-weight-bold' };
|
|
15
|
+
const SUFFIX = { text: 'per year' };
|
|
14
16
|
|
|
15
17
|
const question = new SingleLineInputQuestion({
|
|
16
18
|
title: TITLE,
|
|
@@ -20,6 +22,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
|
|
|
20
22
|
html: HTML,
|
|
21
23
|
hint: HINT,
|
|
22
24
|
label: LABEL,
|
|
25
|
+
prefix: PREFIX,
|
|
26
|
+
suffix: SUFFIX,
|
|
23
27
|
autocomplete: FIELDNAME
|
|
24
28
|
});
|
|
25
29
|
|
|
@@ -31,6 +35,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
|
|
|
31
35
|
assert.strictEqual(question.html, HTML);
|
|
32
36
|
assert.strictEqual(question.hint, HINT);
|
|
33
37
|
assert.strictEqual(question.label, LABEL);
|
|
38
|
+
assert.strictEqual(question.prefix, PREFIX);
|
|
39
|
+
assert.strictEqual(question.suffix, SUFFIX);
|
|
34
40
|
assert.strictEqual(question.autocomplete, FIELDNAME);
|
|
35
41
|
});
|
|
36
42
|
});
|
|
@@ -54,7 +54,7 @@ export class UnitOptionEntryQuestion extends Question {
|
|
|
54
54
|
/**
|
|
55
55
|
* Get the data to save from the request, returns an object of answers
|
|
56
56
|
* @param {import('express').Request} req
|
|
57
|
-
* @param {JourneyResponse} journeyResponse - current journey response
|
|
57
|
+
* @param {JourneyResponse} journeyResponse - current journey response
|
|
58
58
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
59
59
|
*/ getDataToSave(req: import("express").Request, journeyResponse: JourneyResponse): Promise<{
|
|
60
60
|
answers: Record<string, unknown>;
|
|
@@ -125,7 +125,7 @@ export class UnitOptionEntryQuestion extends Question {
|
|
|
125
125
|
/**
|
|
126
126
|
* Get the data to save from the request, returns an object of answers
|
|
127
127
|
* @param {import('express').Request} req
|
|
128
|
-
* @param {JourneyResponse} journeyResponse - current journey response
|
|
128
|
+
* @param {JourneyResponse} journeyResponse - current journey response
|
|
129
129
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
130
130
|
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
131
131
|
async getDataToSave(req, journeyResponse) {
|
package/src/controller.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, mock
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import { list, question, buildSave } from './controller.js';
|
|
4
4
|
|
|
@@ -9,214 +9,210 @@ import { mockReq, mockRes } from '#test/utils/utils.js';
|
|
|
9
9
|
import { BOOLEAN_OPTIONS } from '#src/components/boolean/question.js';
|
|
10
10
|
import { MANAGE_LIST_ACTIONS } from '#src/components/manage-list/manage-list-actions.js';
|
|
11
11
|
|
|
12
|
-
const res = mockRes();
|
|
13
12
|
const mockBaseUrl = '/manage-appeals/questionnaire';
|
|
14
13
|
const mockRef = '123456';
|
|
15
14
|
const mockTemplateUrl = 'template.njk';
|
|
16
15
|
const mockListingPath = 'mockListingPath.njk';
|
|
17
16
|
const mockJourneyTitle = 'Mock Manage Appeals';
|
|
18
17
|
const mockAnswer = 'Not started';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return true;
|
|
28
|
-
},
|
|
29
|
-
questions: [
|
|
30
|
-
{
|
|
31
|
-
title: 'Title 1a',
|
|
32
|
-
question: 'Why?',
|
|
33
|
-
taskList: true,
|
|
34
|
-
fieldName: 'title-1a',
|
|
35
|
-
shouldDisplay: () => true,
|
|
36
|
-
formatAnswerForSummary: mock.fn()
|
|
18
|
+
|
|
19
|
+
function testSetup() {
|
|
20
|
+
const sections = [
|
|
21
|
+
{
|
|
22
|
+
name: 'Section 1',
|
|
23
|
+
segment: 'segment-1',
|
|
24
|
+
getStatus: () => {
|
|
25
|
+
return SECTION_STATUS.COMPLETE;
|
|
37
26
|
},
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
isComplete: () => {
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
questions: [
|
|
31
|
+
{
|
|
32
|
+
title: 'Title 1a',
|
|
33
|
+
question: 'Why?',
|
|
34
|
+
taskList: true,
|
|
35
|
+
fieldName: 'title-1a',
|
|
36
|
+
shouldDisplay: () => true,
|
|
37
|
+
formatAnswerForSummary: mock.fn(() => [
|
|
38
|
+
{
|
|
39
|
+
key: 'Title 1a',
|
|
40
|
+
value: mockAnswer,
|
|
41
|
+
action: {
|
|
42
|
+
href: '/manage-appeals/questionnaire/123456/segment-1/title-1a',
|
|
43
|
+
text: 'Answer',
|
|
44
|
+
visuallyHiddenText: 'Answer'
|
|
45
|
+
}
|
|
52
46
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
question: 'How?',
|
|
71
|
-
taskList: true,
|
|
72
|
-
fieldName: 'title-2a',
|
|
73
|
-
shouldDisplay: () => true,
|
|
74
|
-
formatAnswerForSummary: () => [
|
|
75
|
-
{
|
|
76
|
-
key: 'Title 2a',
|
|
77
|
-
value: mockAnswer,
|
|
78
|
-
action: {
|
|
79
|
-
href: '/manage-appeals/questionnaire/123456/segment-2/title-2a',
|
|
80
|
-
text: 'Answer',
|
|
81
|
-
visuallyHiddenText: 'Answer'
|
|
47
|
+
])
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: 'Title 1b',
|
|
51
|
+
question: 'Who?',
|
|
52
|
+
taskList: false,
|
|
53
|
+
fieldName: 'title-1b',
|
|
54
|
+
shouldDisplay: () => true,
|
|
55
|
+
formatAnswerForSummary: mock.fn(() => [
|
|
56
|
+
{
|
|
57
|
+
key: 'Title 1b',
|
|
58
|
+
value: mockAnswer,
|
|
59
|
+
action: {
|
|
60
|
+
href: '/manage-appeals/questionnaire/123456/segment-1/title-1b',
|
|
61
|
+
text: 'Answer',
|
|
62
|
+
visuallyHiddenText: 'Answer'
|
|
63
|
+
}
|
|
82
64
|
}
|
|
83
|
-
|
|
84
|
-
|
|
65
|
+
])
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'Section 2',
|
|
71
|
+
segment: 'segment-2',
|
|
72
|
+
getStatus: () => {
|
|
73
|
+
return SECTION_STATUS.IN_PROGRESS;
|
|
85
74
|
},
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
75
|
+
isComplete: () => {
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
questions: [
|
|
79
|
+
{
|
|
80
|
+
title: 'Title 2a',
|
|
81
|
+
question: 'How?',
|
|
82
|
+
taskList: true,
|
|
83
|
+
fieldName: 'title-2a',
|
|
84
|
+
shouldDisplay: () => true,
|
|
85
|
+
formatAnswerForSummary: () => [
|
|
86
|
+
{
|
|
87
|
+
key: 'Title 2a',
|
|
88
|
+
value: mockAnswer,
|
|
89
|
+
action: {
|
|
90
|
+
href: '/manage-appeals/questionnaire/123456/segment-2/title-2a',
|
|
91
|
+
text: 'Answer',
|
|
92
|
+
visuallyHiddenText: 'Answer'
|
|
93
|
+
}
|
|
100
94
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
fieldName: 'title-3a',
|
|
119
|
-
shouldDisplay: () => true,
|
|
120
|
-
formatAnswerForSummary: () => [
|
|
121
|
-
{
|
|
122
|
-
key: 'Title 3a',
|
|
123
|
-
value: mockAnswer,
|
|
124
|
-
action: {
|
|
125
|
-
href: '/manage-appeals/questionnaire/123456/segment-3/title-3a',
|
|
126
|
-
text: 'Answer',
|
|
127
|
-
visuallyHiddenText: 'Answer'
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
title: 'Title 2b',
|
|
99
|
+
question: 'What?',
|
|
100
|
+
taskList: true,
|
|
101
|
+
fieldName: 'title-2b',
|
|
102
|
+
shouldDisplay: () => true,
|
|
103
|
+
formatAnswerForSummary: () => [
|
|
104
|
+
{
|
|
105
|
+
key: 'Title 2b',
|
|
106
|
+
value: mockAnswer,
|
|
107
|
+
action: {
|
|
108
|
+
href: '/manage-appeals/questionnaire/123456/segment-2/title-2b',
|
|
109
|
+
text: 'Answer',
|
|
110
|
+
visuallyHiddenText: 'Answer'
|
|
111
|
+
}
|
|
128
112
|
}
|
|
129
|
-
|
|
130
|
-
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'Section 3',
|
|
119
|
+
segment: 'segment-3',
|
|
120
|
+
getStatus: () => {
|
|
121
|
+
return SECTION_STATUS.NOT_STARTED;
|
|
131
122
|
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
123
|
+
isComplete: mock.fn(),
|
|
124
|
+
questions: [
|
|
125
|
+
{
|
|
126
|
+
title: 'Title 3a',
|
|
127
|
+
question: 'When?',
|
|
128
|
+
taskList: false,
|
|
129
|
+
fieldName: 'title-3a',
|
|
130
|
+
shouldDisplay: () => true,
|
|
131
|
+
formatAnswerForSummary: () => [
|
|
132
|
+
{
|
|
133
|
+
key: 'Title 3a',
|
|
134
|
+
value: mockAnswer,
|
|
135
|
+
action: {
|
|
136
|
+
href: '/manage-appeals/questionnaire/123456/segment-3/title-3a',
|
|
137
|
+
text: 'Answer',
|
|
138
|
+
visuallyHiddenText: 'Answer'
|
|
139
|
+
}
|
|
146
140
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
title: 'Title 3b',
|
|
145
|
+
question: 'Really?',
|
|
146
|
+
taskList: true,
|
|
147
|
+
fieldName: 'title-3b',
|
|
148
|
+
shouldDisplay: () => true,
|
|
149
|
+
formatAnswerForSummary: () => [
|
|
150
|
+
{
|
|
151
|
+
key: 'Title 3b',
|
|
152
|
+
value: mockAnswer,
|
|
153
|
+
action: {
|
|
154
|
+
href: '/manage-appeals/questionnaire/123456/segment-3/title-3b',
|
|
155
|
+
text: 'Answer',
|
|
156
|
+
visuallyHiddenText: 'Answer'
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const journeyParams = {
|
|
166
|
+
sections,
|
|
167
|
+
journeyId: 'TEST',
|
|
168
|
+
referenceId: 'REF',
|
|
169
|
+
taskListUrl: 'task-list',
|
|
170
|
+
makeBaseUrl: () => `${mockBaseUrl}/${mockRef}`,
|
|
171
|
+
journeyTemplate: mockTemplateUrl,
|
|
172
|
+
taskListTemplate: mockListingPath,
|
|
173
|
+
journeyTitle: mockJourneyTitle
|
|
174
|
+
};
|
|
164
175
|
|
|
165
|
-
const mockResponse = {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
};
|
|
176
|
+
const mockResponse = {
|
|
177
|
+
referenceId: mockRef,
|
|
178
|
+
answers: {
|
|
179
|
+
'title-1a': 'yes',
|
|
180
|
+
'title-2a': null,
|
|
181
|
+
'title-2b': undefined
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const res = mockRes();
|
|
185
|
+
const req = mockReq(null);
|
|
186
|
+
|
|
187
|
+
const mockJourney = new Journey({ response: mockResponse, ...journeyParams });
|
|
188
|
+
|
|
189
|
+
res.locals.journeyResponse = {};
|
|
190
|
+
res.locals.journey = mockJourney;
|
|
191
|
+
|
|
192
|
+
const sampleQuestionObj = {
|
|
193
|
+
fieldName: 'sampleFieldName',
|
|
194
|
+
renderAction: mock.fn(),
|
|
195
|
+
getDataToSave: mock.fn(),
|
|
196
|
+
checkForValidationErrors: mock.fn(),
|
|
197
|
+
checkForSavingErrors: mock.fn(),
|
|
198
|
+
toViewModel: mock.fn(),
|
|
199
|
+
prepQuestionForRendering: mock.fn(),
|
|
200
|
+
formatAnswerForSummary: mock.fn(() => [mockAnswer]),
|
|
201
|
+
viewFolder: 'sampleType'
|
|
202
|
+
};
|
|
173
203
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
fieldName: 'sampleFieldName',
|
|
179
|
-
renderAction: mock.fn(),
|
|
180
|
-
getDataToSave: mock.fn(),
|
|
181
|
-
checkForValidationErrors: mock.fn(),
|
|
182
|
-
checkForSavingErrors: mock.fn(),
|
|
183
|
-
toViewModel: mock.fn(),
|
|
184
|
-
prepQuestionForRendering: mock.fn(),
|
|
185
|
-
formatAnswerForSummary: mock.fn(() => [mockAnswer]),
|
|
186
|
-
viewFolder: 'sampleType'
|
|
187
|
-
};
|
|
204
|
+
const mockSection = {
|
|
205
|
+
name: '123',
|
|
206
|
+
segment: 'test'
|
|
207
|
+
};
|
|
188
208
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
segment: 'test'
|
|
192
|
-
};
|
|
209
|
+
return { req, res, mockJourney, sampleQuestionObj, mockSection, journeyParams };
|
|
210
|
+
}
|
|
193
211
|
|
|
194
212
|
describe('dynamic-form/controller', () => {
|
|
195
|
-
let req;
|
|
196
|
-
beforeEach(() => {
|
|
197
|
-
res.locals.journeyResponse = {};
|
|
198
|
-
mockJourney = new Journey({ response: mockResponse, ...journeyParams });
|
|
199
|
-
res.locals.journey = mockJourney;
|
|
200
|
-
mockJourney.sections[0].questions[0].formatAnswerForSummary.mock.mockImplementation(() => [
|
|
201
|
-
{
|
|
202
|
-
key: 'Title 1a',
|
|
203
|
-
value: mockAnswer,
|
|
204
|
-
action: {
|
|
205
|
-
href: '/manage-appeals/questionnaire/123456/segment-1/title-1a',
|
|
206
|
-
text: 'Answer',
|
|
207
|
-
visuallyHiddenText: 'Answer'
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
]);
|
|
211
|
-
mockJourney.sections[2].isComplete = mock.fn(() => false);
|
|
212
|
-
|
|
213
|
-
mockSummaryListData = _getmockSummaryListData(mockJourney);
|
|
214
|
-
req = mockReq(null);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
213
|
describe('list', () => {
|
|
218
214
|
it('should render the view correctly', async () => {
|
|
219
|
-
res
|
|
215
|
+
const { req, res, mockJourney } = testSetup();
|
|
220
216
|
const appeal = { a: 1, caseReference: 2 };
|
|
221
217
|
|
|
222
218
|
const pageCaption = `Appeal ${appeal.caseReference}`;
|
|
@@ -228,7 +224,7 @@ describe('dynamic-form/controller', () => {
|
|
|
228
224
|
'components/task-list/index',
|
|
229
225
|
{
|
|
230
226
|
appeal,
|
|
231
|
-
summaryListData:
|
|
227
|
+
summaryListData: _getmockSummaryListData(mockJourney),
|
|
232
228
|
layoutTemplate: mockListingPath,
|
|
233
229
|
journeyComplete: false,
|
|
234
230
|
pageCaption: pageCaption,
|
|
@@ -237,8 +233,8 @@ describe('dynamic-form/controller', () => {
|
|
|
237
233
|
]);
|
|
238
234
|
});
|
|
239
235
|
it('support array of actions', async () => {
|
|
236
|
+
const { req, res, mockJourney } = testSetup();
|
|
240
237
|
const mockFn = res.render.mock;
|
|
241
|
-
mockFn.resetCalls();
|
|
242
238
|
mockJourney.sections[0].questions[0].formatAnswerForSummary.mock.mockImplementation(() => [
|
|
243
239
|
{
|
|
244
240
|
key: 'Title 1a',
|
|
@@ -257,13 +253,14 @@ describe('dynamic-form/controller', () => {
|
|
|
257
253
|
]
|
|
258
254
|
}
|
|
259
255
|
]);
|
|
260
|
-
|
|
256
|
+
|
|
261
257
|
const appeal = { a: 1, caseReference: 2 };
|
|
262
258
|
|
|
263
259
|
const pageCaption = `Appeal ${appeal.caseReference}`;
|
|
264
260
|
await list(req, res, pageCaption, { appeal });
|
|
265
261
|
|
|
266
|
-
|
|
262
|
+
const expectSummaryList = _getmockSummaryListData(mockJourney);
|
|
263
|
+
expectSummaryList.sections[0].list.rows[0].actions.items.push({
|
|
267
264
|
href: '/manage-appeals/questionnaire/123456/segment-1/title-1a/edit',
|
|
268
265
|
text: 'Edit',
|
|
269
266
|
visuallyHiddenText: 'Edit'
|
|
@@ -274,7 +271,7 @@ describe('dynamic-form/controller', () => {
|
|
|
274
271
|
'components/task-list/index',
|
|
275
272
|
{
|
|
276
273
|
appeal,
|
|
277
|
-
summaryListData:
|
|
274
|
+
summaryListData: expectSummaryList,
|
|
278
275
|
layoutTemplate: mockListingPath,
|
|
279
276
|
journeyComplete: false,
|
|
280
277
|
pageCaption: pageCaption,
|
|
@@ -284,9 +281,8 @@ describe('dynamic-form/controller', () => {
|
|
|
284
281
|
});
|
|
285
282
|
|
|
286
283
|
it('should format answer summary including conditional answer', async () => {
|
|
284
|
+
const { req, res, mockJourney } = testSetup();
|
|
287
285
|
const mockFn = mockJourney.sections[0].questions[0].formatAnswerForSummary.mock;
|
|
288
|
-
mockFn.resetCalls();
|
|
289
|
-
res.locals.journeyResponse.referenceId = mockRef;
|
|
290
286
|
|
|
291
287
|
await list(req, res);
|
|
292
288
|
|
|
@@ -300,8 +296,8 @@ describe('dynamic-form/controller', () => {
|
|
|
300
296
|
assert.strictEqual(args[2], expectedAnswer.value);
|
|
301
297
|
});
|
|
302
298
|
it('should format an undefined action as undefined', async () => {
|
|
299
|
+
const { req, res, mockJourney } = testSetup();
|
|
303
300
|
const mockFn = res.render.mock;
|
|
304
|
-
mockFn.resetCalls();
|
|
305
301
|
mockJourney.sections[0].questions[0].formatAnswerForSummary.mock.mockImplementation(() => [
|
|
306
302
|
{
|
|
307
303
|
key: 'Title 1a',
|
|
@@ -309,7 +305,7 @@ describe('dynamic-form/controller', () => {
|
|
|
309
305
|
action: undefined
|
|
310
306
|
}
|
|
311
307
|
]);
|
|
312
|
-
|
|
308
|
+
|
|
313
309
|
const appeal = { a: 1, caseReference: 2 };
|
|
314
310
|
|
|
315
311
|
const pageCaption = `Appeal ${appeal.caseReference}`;
|
|
@@ -321,8 +317,8 @@ describe('dynamic-form/controller', () => {
|
|
|
321
317
|
});
|
|
322
318
|
|
|
323
319
|
it('should format a single action object as an array in the actions property', async () => {
|
|
320
|
+
const { req, res, mockJourney } = testSetup();
|
|
324
321
|
const mockFn = res.render.mock;
|
|
325
|
-
mockFn.resetCalls();
|
|
326
322
|
mockJourney.sections[0].questions[0].formatAnswerForSummary.mock.mockImplementation(() => [
|
|
327
323
|
{
|
|
328
324
|
key: 'Title 1a',
|
|
@@ -334,7 +330,6 @@ describe('dynamic-form/controller', () => {
|
|
|
334
330
|
}
|
|
335
331
|
}
|
|
336
332
|
]);
|
|
337
|
-
res.locals.journeyResponse.referenceId = mockRef;
|
|
338
333
|
|
|
339
334
|
await list(req, res);
|
|
340
335
|
|
|
@@ -353,6 +348,7 @@ describe('dynamic-form/controller', () => {
|
|
|
353
348
|
|
|
354
349
|
describe('question', () => {
|
|
355
350
|
it('should redirect if question is not found', async () => {
|
|
351
|
+
const { req, res, mockJourney } = testSetup();
|
|
356
352
|
mockJourney.getQuestionByParams = mock.fn();
|
|
357
353
|
mockJourney.getQuestionByParams.mock.mockImplementationOnce(() => null);
|
|
358
354
|
|
|
@@ -362,6 +358,7 @@ describe('dynamic-form/controller', () => {
|
|
|
362
358
|
});
|
|
363
359
|
|
|
364
360
|
it('should use custom action if renderAction is defined', async () => {
|
|
361
|
+
const { req, res, mockJourney, sampleQuestionObj } = testSetup();
|
|
365
362
|
mockJourney.getSection = mock.fn();
|
|
366
363
|
mockJourney.getSection.mock.mockImplementationOnce(() => {
|
|
367
364
|
return {};
|
|
@@ -377,8 +374,7 @@ describe('dynamic-form/controller', () => {
|
|
|
377
374
|
});
|
|
378
375
|
|
|
379
376
|
it('should render the question template', async () => {
|
|
380
|
-
sampleQuestionObj
|
|
381
|
-
sampleQuestionObj.toViewModel.mock.resetCalls();
|
|
377
|
+
const { req, res, mockJourney, sampleQuestionObj, mockSection } = testSetup();
|
|
382
378
|
req.params.referenceId = mockRef;
|
|
383
379
|
const mockAnswer = 'sampleAnswer';
|
|
384
380
|
const mockBackLink = 'back';
|
|
@@ -404,7 +400,7 @@ describe('dynamic-form/controller', () => {
|
|
|
404
400
|
});
|
|
405
401
|
|
|
406
402
|
it('should redirect to tasklist when manage list action is remove and journey response has no answers', async () => {
|
|
407
|
-
res
|
|
403
|
+
const { req, res, mockJourney, sampleQuestionObj, mockSection } = testSetup();
|
|
408
404
|
|
|
409
405
|
sampleQuestionObj.isManageListQuestion = true;
|
|
410
406
|
sampleQuestionObj.renderConfirmationAction = mock.fn();
|
|
@@ -433,7 +429,7 @@ describe('dynamic-form/controller', () => {
|
|
|
433
429
|
});
|
|
434
430
|
|
|
435
431
|
it('should redirect to tasklist when manage list action is remove and the manage list answer is undefined', async () => {
|
|
436
|
-
res
|
|
432
|
+
const { req, res, mockJourney, sampleQuestionObj, mockSection } = testSetup();
|
|
437
433
|
|
|
438
434
|
sampleQuestionObj.isManageListQuestion = true;
|
|
439
435
|
sampleQuestionObj.renderConfirmationAction = mock.fn();
|
|
@@ -464,7 +460,7 @@ describe('dynamic-form/controller', () => {
|
|
|
464
460
|
});
|
|
465
461
|
|
|
466
462
|
it('should redirect to tasklist when manage list action is remove and the manage list answer is not an array', async () => {
|
|
467
|
-
res
|
|
463
|
+
const { req, res, mockJourney, sampleQuestionObj, mockSection } = testSetup();
|
|
468
464
|
|
|
469
465
|
sampleQuestionObj.isManageListQuestion = true;
|
|
470
466
|
sampleQuestionObj.renderConfirmationAction = mock.fn();
|
|
@@ -495,7 +491,7 @@ describe('dynamic-form/controller', () => {
|
|
|
495
491
|
});
|
|
496
492
|
|
|
497
493
|
it('should render the confirmation page when manage list action is remove and the question has a renderConfirmationAction function', async () => {
|
|
498
|
-
res
|
|
494
|
+
const { req, res, mockJourney, sampleQuestionObj, mockSection } = testSetup();
|
|
499
495
|
|
|
500
496
|
sampleQuestionObj.isManageListQuestion = true;
|
|
501
497
|
|
|
@@ -529,6 +525,7 @@ describe('dynamic-form/controller', () => {
|
|
|
529
525
|
|
|
530
526
|
describe('save', () => {
|
|
531
527
|
it('should use saveData', async () => {
|
|
528
|
+
const { req, res, mockJourney, sampleQuestionObj, journeyParams } = testSetup();
|
|
532
529
|
const journeyId = 'has-questionnaire';
|
|
533
530
|
|
|
534
531
|
req.params = {
|
|
@@ -573,12 +570,13 @@ describe('dynamic-form/controller', () => {
|
|
|
573
570
|
for (const mockFn of mocks) {
|
|
574
571
|
assert.strictEqual(mockFn.callCount(), 1);
|
|
575
572
|
assert.deepStrictEqual(mockFn.calls[0].arguments[0], req);
|
|
576
|
-
assert.deepStrictEqual(mockFn.calls[0].arguments[1], sections[0]);
|
|
573
|
+
assert.deepStrictEqual(mockFn.calls[0].arguments[1], mockJourney.sections[0]);
|
|
577
574
|
assert.deepStrictEqual(mockFn.calls[0].arguments[2], mockJourney);
|
|
578
575
|
}
|
|
579
576
|
});
|
|
580
577
|
|
|
581
578
|
it('should handle error', async () => {
|
|
579
|
+
const { req, res, mockJourney, sampleQuestionObj, journeyParams } = testSetup();
|
|
582
580
|
const journeyId = 'has-questionnaire';
|
|
583
581
|
const expectedViewModel = { a: 1 };
|
|
584
582
|
const sampleQuestionObjWithActions = {
|
|
@@ -639,6 +637,7 @@ describe('dynamic-form/controller', () => {
|
|
|
639
637
|
});
|
|
640
638
|
|
|
641
639
|
it('should handle validation errors', async () => {
|
|
640
|
+
const { req, res, mockJourney, sampleQuestionObj, journeyParams } = testSetup();
|
|
642
641
|
const expectedErrors = {
|
|
643
642
|
errorViewModel: 'mocked-validation-error'
|
|
644
643
|
};
|
|
@@ -671,7 +670,7 @@ describe('dynamic-form/controller', () => {
|
|
|
671
670
|
});
|
|
672
671
|
|
|
673
672
|
it('should handle saving', async () => {
|
|
674
|
-
res
|
|
673
|
+
const { req, res, mockJourney, sampleQuestionObj, journeyParams } = testSetup();
|
|
675
674
|
const journeyId = 'has-questionnaire';
|
|
676
675
|
const expectedUrl = 'redirect-url';
|
|
677
676
|
const sampleQuestionObjWithActions = {
|
|
@@ -713,7 +712,7 @@ describe('dynamic-form/controller', () => {
|
|
|
713
712
|
});
|
|
714
713
|
|
|
715
714
|
it('should redirect to task list if configured', async () => {
|
|
716
|
-
res
|
|
715
|
+
const { req, res, mockJourney, sampleQuestionObj, journeyParams } = testSetup();
|
|
717
716
|
const journeyId = 'has-questionnaire';
|
|
718
717
|
const expectedUrl = 'redirect-url';
|
|
719
718
|
const sampleQuestionObjWithActions = {
|
|
@@ -147,7 +147,7 @@ export class OptionsQuestion extends Question {
|
|
|
147
147
|
/**
|
|
148
148
|
* Get the data to save from the request, returns an object of answers
|
|
149
149
|
* @param {import('express').Request} req
|
|
150
|
-
* @param {import('../journey/journey-response.js').JourneyResponse} journeyResponse - current journey response
|
|
150
|
+
* @param {import('../journey/journey-response.js').JourneyResponse} journeyResponse - current journey response
|
|
151
151
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
152
152
|
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
153
153
|
async getDataToSave(req, journeyResponse) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email-validator.d.ts","sourceRoot":"","sources":["email-validator.js"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AAEH;IACC;;;;;OAKG;IACH,mDAJG;QAAwC,OAAO;QACvB,YAAY;QACZ,SAAS;KACnC,
|
|
1
|
+
{"version":3,"file":"email-validator.d.ts","sourceRoot":"","sources":["email-validator.js"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AAEH;IACC;;;;;OAKG;IACH,mDAJG;QAAwC,OAAO;QACvB,YAAY;QACZ,SAAS;KACnC,EAkBA;IAXA;;;;;;;;;;;;;;;;;;;;;;;;;MAMC;IAID,8BAA0B;IAG3B,wEAIC;CACD;;;;;;;;;;;;;;;;;;;;;;;;0BA1CyB,qBAAqB"}
|
|
@@ -21,6 +21,9 @@ export class EmailValidator extends BaseValidator {
|
|
|
21
21
|
constructor({ options = {}, errorMessage, fieldName } = {}) {
|
|
22
22
|
super();
|
|
23
23
|
|
|
24
|
+
// snake_case to match the express-validator options for isEmail
|
|
25
|
+
// https://express-validator.github.io/docs/api/validation-chain#isemail
|
|
26
|
+
/* eslint-disable camelcase */
|
|
24
27
|
this.options = {
|
|
25
28
|
allow_display_name: options.allowDisplayName || false,
|
|
26
29
|
require_tld: options.requireTld !== false, // Default to true
|
|
@@ -28,6 +31,7 @@ export class EmailValidator extends BaseValidator {
|
|
|
28
31
|
allow_ip_domain: options.allowIpDomain || false,
|
|
29
32
|
...options
|
|
30
33
|
};
|
|
34
|
+
/* eslint-enable camelcase */
|
|
31
35
|
|
|
32
36
|
this.errorMessage = errorMessage || 'Enter an email address in the correct format, like name@example.com';
|
|
33
37
|
this.fieldName = fieldName;
|
|
@@ -68,7 +68,7 @@ export type SelectableOption = {
|
|
|
68
68
|
attributes?: Record<string, string>;
|
|
69
69
|
behaviour?: 'exclusive';
|
|
70
70
|
conditional?: {
|
|
71
|
-
question
|
|
71
|
+
question?: string;
|
|
72
72
|
type: string;
|
|
73
73
|
fieldName: string;
|
|
74
74
|
inputClasses?: string;
|
|
@@ -261,6 +261,8 @@ export type SingleLineInputQuestionParams = CommonQuestionParams & {
|
|
|
261
261
|
inputAttributes?: Record<string, string>; // HTML attributes to add to the input
|
|
262
262
|
label?: string; // if defined this will show as a label for the input and the question will just be a standard h1
|
|
263
263
|
classes?: string; // HTML classes to add to the input
|
|
264
|
+
prefix?: Affix;
|
|
265
|
+
suffix?: Affix;
|
|
264
266
|
};
|
|
265
267
|
|
|
266
268
|
type SingleLineInputQuestionProps = SingleLineInputQuestionParams & {
|