@planning-inspectorate/dynamic-forms 3.20.4 → 3.20.5
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/package.json +1 -1
- package/src/components/boolean/question.d.ts +20 -1
- package/src/components/boolean/question.d.ts.map +1 -1
- package/src/components/boolean/question.js +45 -0
- package/src/components/boolean/question.test.js +22 -1
- package/src/controller.test.js +207 -208
- package/src/validator/email-validator.d.ts.map +1 -1
- package/src/validator/email-validator.js +4 -0
|
@@ -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/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
|
/**
|
|
@@ -19,7 +22,23 @@ export class BooleanQuestion extends RadioQuestion {
|
|
|
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
|
};
|
|
@@ -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
|
});
|
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 = {
|
|
@@ -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;
|