@planning-inspectorate/dynamic-forms 3.3.0 → 3.3.2
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 +2 -4
- package/src/components/boolean/question.js +6 -11
- package/src/components/date/question.js +5 -11
- package/src/components/date-period/question.js +5 -9
- package/src/components/date-time/question.js +5 -8
- package/src/components/date-time/question.test.js +3 -24
- package/src/components/manage-list/question.js +9 -5
- package/src/components/manage-list/question.test.js +38 -1
- package/src/components/manage-list/utils.js +21 -0
- package/src/components/manage-list/utils.test.js +34 -1
- package/src/components/multi-field-input/question.js +7 -11
- package/src/components/multi-field-input/question.test.js +29 -1
- package/src/components/text-entry-redact/question.js +3 -4
- package/src/components/unit-option-entry/question.js +6 -13
- package/src/controller.js +18 -0
- package/src/controller.test.js +19 -1
- package/src/lib/uuid.test.js +0 -12
- package/src/questions/options-question.js +6 -13
- package/src/questions/question.js +8 -22
- package/src/questions/question.test.js +3 -19
- package/test/check-your-answers.test.js +1 -1
- package/test/mock/uuid.js +13 -0
- package/test/questions.test.js +3 -200
- package/test/utils/question-test-utils.js +200 -0
package/package.json
CHANGED
|
@@ -72,12 +72,11 @@ export default class AddressQuestion extends Question {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
* side effect: modifies journeyResponse with the new answers
|
|
75
|
+
* Get the data to save from the request, returns an object of answers
|
|
77
76
|
* @param {import('express').Request} req
|
|
78
77
|
* @param {JourneyResponse} journeyResponse
|
|
79
78
|
* @returns {Promise<{answers: Record<string, unknown>}>}
|
|
80
|
-
*/
|
|
79
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
81
80
|
async getDataToSave(req, journeyResponse) {
|
|
82
81
|
const data = {
|
|
83
82
|
addressLine1: req.body[this.fieldName + '_addressLine1'],
|
|
@@ -94,7 +93,6 @@ export default class AddressQuestion extends Question {
|
|
|
94
93
|
const answers = {
|
|
95
94
|
[this.fieldName]: address
|
|
96
95
|
};
|
|
97
|
-
journeyResponse.answers[this.fieldName] = address;
|
|
98
96
|
|
|
99
97
|
return {
|
|
100
98
|
answers
|
|
@@ -97,28 +97,23 @@ export default class BooleanQuestion extends RadioQuestion {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
101
|
-
* side effect: modifies journeyResponse with the new answers
|
|
100
|
+
* Get the data to save from the request, returns an object of answers
|
|
102
101
|
* @param {import('express').Request} req
|
|
103
102
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
104
103
|
* @returns {Promise.<Object>}
|
|
105
|
-
*/
|
|
104
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
106
105
|
async getDataToSave(req, journeyResponse) {
|
|
107
|
-
|
|
108
|
-
let responseToSave = { answers: {} };
|
|
106
|
+
const answers = {};
|
|
109
107
|
const fieldValue = req.body[this.fieldName]?.trim();
|
|
110
108
|
|
|
111
|
-
|
|
109
|
+
answers[this.fieldName] = fieldValue === BOOLEAN_OPTIONS.YES;
|
|
112
110
|
|
|
113
111
|
for (const propName in req.body) {
|
|
114
112
|
if (propName.startsWith(this.fieldName + '_')) {
|
|
115
|
-
|
|
116
|
-
journeyResponse.answers[propName] = req.body[propName]?.trim();
|
|
113
|
+
answers[propName] = req.body[propName]?.trim();
|
|
117
114
|
}
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return responseToSave;
|
|
117
|
+
return { answers };
|
|
123
118
|
}
|
|
124
119
|
}
|
|
@@ -27,27 +27,21 @@ export default class DateQuestion extends Question {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* side effect: modifies journeyResponse with the new answers
|
|
30
|
+
* Get the data to save from the request, returns an object of answers
|
|
32
31
|
* @param {import('express').Request} req
|
|
33
32
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
34
33
|
* @returns {Promise.<Object>}
|
|
35
|
-
*/
|
|
34
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
36
35
|
async getDataToSave(req, journeyResponse) {
|
|
37
|
-
|
|
38
|
-
let responseToSave = { answers: {} };
|
|
36
|
+
const answers = {};
|
|
39
37
|
|
|
40
38
|
const dayInput = req.body[`${this.fieldName}_day`];
|
|
41
39
|
const monthInput = req.body[`${this.fieldName}_month`];
|
|
42
40
|
const yearInput = req.body[`${this.fieldName}_year`];
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
responseToSave.answers[this.fieldName] = dateToSave;
|
|
47
|
-
|
|
48
|
-
journeyResponse.answers[this.fieldName] = responseToSave.answers[this.fieldName];
|
|
42
|
+
answers[this.fieldName] = parseDateInput({ day: dayInput, month: monthInput, year: yearInput });
|
|
49
43
|
|
|
50
|
-
return
|
|
44
|
+
return { answers };
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
answerForViewModel(answers, isPayload) {
|
|
@@ -36,15 +36,13 @@ export default class DatePeriodQuestion extends Question {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
* side effect: modifies journeyResponse with the new answers
|
|
39
|
+
* Get the data to save from the request, returns an object of answers
|
|
41
40
|
* @param {import('express').Request} req
|
|
42
41
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
43
42
|
* @returns {Promise.<Object>}
|
|
44
|
-
*/
|
|
43
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
45
44
|
async getDataToSave(req, journeyResponse) {
|
|
46
|
-
|
|
47
|
-
let responseToSave = { answers: {} };
|
|
45
|
+
const answers = {};
|
|
48
46
|
|
|
49
47
|
const startDayInput = req.body[`${this.fieldName}_start_day`];
|
|
50
48
|
const startMonthInput = req.body[`${this.fieldName}_start_month`];
|
|
@@ -70,11 +68,9 @@ export default class DatePeriodQuestion extends Question {
|
|
|
70
68
|
year: endYearInput
|
|
71
69
|
});
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
journeyResponse.answers[this.fieldName] = responseToSave.answers[this.fieldName];
|
|
71
|
+
answers[this.fieldName] = { start: startDate, end: endDate };
|
|
76
72
|
|
|
77
|
-
return
|
|
73
|
+
return { answers };
|
|
78
74
|
}
|
|
79
75
|
|
|
80
76
|
answerForViewModel(answers, isPayload) {
|
|
@@ -23,14 +23,13 @@ export default class DateTimeQuestion extends Question {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
* side effect: modifies journeyResponse with the new answers
|
|
26
|
+
* Get the data to save from the request, returns an object of answers
|
|
28
27
|
* @param {import('express').Request} req
|
|
29
28
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
30
29
|
* @returns {Promise.<Object>}
|
|
31
|
-
*/
|
|
30
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
32
31
|
async getDataToSave(req, journeyResponse) {
|
|
33
|
-
|
|
32
|
+
const answers = {};
|
|
34
33
|
|
|
35
34
|
const dayInput = req.body[`${this.fieldName}_day`];
|
|
36
35
|
const monthInput = req.body[`${this.fieldName}_month`];
|
|
@@ -39,7 +38,7 @@ export default class DateTimeQuestion extends Question {
|
|
|
39
38
|
const minutesInput = req.body[`${this.fieldName}_minutes`];
|
|
40
39
|
const periodInput = req.body[`${this.fieldName}_period`];
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
answers[this.fieldName] = parseDateInput({
|
|
43
42
|
day: dayInput,
|
|
44
43
|
month: monthInput,
|
|
45
44
|
year: yearInput,
|
|
@@ -47,9 +46,7 @@ export default class DateTimeQuestion extends Question {
|
|
|
47
46
|
minute: minutesInput
|
|
48
47
|
});
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return responseToSave;
|
|
49
|
+
return { answers };
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
answerForViewModel(answers, isPayload) {
|
|
@@ -131,20 +131,13 @@ describe('./lib/forms/custom-components/date-time/question.js', () => {
|
|
|
131
131
|
siteVisitDate_period: 'am'
|
|
132
132
|
}
|
|
133
133
|
};
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const responseToSave = await question.getDataToSave(req, journeyResponse);
|
|
134
|
+
const responseToSave = await question.getDataToSave(req);
|
|
137
135
|
|
|
138
136
|
assert.deepStrictEqual(responseToSave, {
|
|
139
137
|
answers: {
|
|
140
138
|
siteVisitDate: new Date('2025-02-21T00:12:00.000Z')
|
|
141
139
|
}
|
|
142
140
|
});
|
|
143
|
-
assert.deepStrictEqual(journeyResponse, {
|
|
144
|
-
answers: {
|
|
145
|
-
siteVisitDate: new Date('2025-02-21T00:12:00.000Z')
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
141
|
});
|
|
149
142
|
it('should get data to save when time period is pm', async () => {
|
|
150
143
|
const req = {
|
|
@@ -157,20 +150,13 @@ describe('./lib/forms/custom-components/date-time/question.js', () => {
|
|
|
157
150
|
siteVisitDate_period: 'pm'
|
|
158
151
|
}
|
|
159
152
|
};
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const responseToSave = await question.getDataToSave(req, journeyResponse);
|
|
153
|
+
const responseToSave = await question.getDataToSave(req);
|
|
163
154
|
|
|
164
155
|
assert.deepStrictEqual(responseToSave, {
|
|
165
156
|
answers: {
|
|
166
157
|
siteVisitDate: new Date('2025-02-21T20:12:00.000Z')
|
|
167
158
|
}
|
|
168
159
|
});
|
|
169
|
-
assert.deepStrictEqual(journeyResponse, {
|
|
170
|
-
answers: {
|
|
171
|
-
siteVisitDate: new Date('2025-02-21T20:12:00.000Z')
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
160
|
});
|
|
175
161
|
it('should get data to save when bst date', async () => {
|
|
176
162
|
const req = {
|
|
@@ -183,20 +169,13 @@ describe('./lib/forms/custom-components/date-time/question.js', () => {
|
|
|
183
169
|
siteVisitDate_period: 'am'
|
|
184
170
|
}
|
|
185
171
|
};
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const responseToSave = await question.getDataToSave(req, journeyResponse);
|
|
172
|
+
const responseToSave = await question.getDataToSave(req);
|
|
189
173
|
|
|
190
174
|
assert.deepStrictEqual(responseToSave, {
|
|
191
175
|
answers: {
|
|
192
176
|
siteVisitDate: new Date('2025-05-20T23:12:00.000Z')
|
|
193
177
|
}
|
|
194
178
|
});
|
|
195
|
-
assert.deepStrictEqual(journeyResponse, {
|
|
196
|
-
answers: {
|
|
197
|
-
siteVisitDate: new Date('2025-05-20T23:12:00.000Z')
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
179
|
});
|
|
201
180
|
});
|
|
202
181
|
describe('formatAnswerForSummary', () => {
|
|
@@ -71,7 +71,10 @@ export default class ManageListQuestion extends Question {
|
|
|
71
71
|
return [];
|
|
72
72
|
}
|
|
73
73
|
const mockJourney = {
|
|
74
|
-
getCurrentQuestionUrl() {}
|
|
74
|
+
getCurrentQuestionUrl() {},
|
|
75
|
+
response: {
|
|
76
|
+
answers: answer
|
|
77
|
+
}
|
|
75
78
|
};
|
|
76
79
|
return (
|
|
77
80
|
this.section.questions
|
|
@@ -92,10 +95,11 @@ export default class ManageListQuestion extends Question {
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
async getDataToSave(req, journeyResponse) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
return {
|
|
99
|
+
answers: {
|
|
100
|
+
[this.fieldName]: journeyResponse.answers[this.fieldName] || []
|
|
101
|
+
}
|
|
102
|
+
};
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
formatAnswerForSummary(sectionSegment, journey, answer) {
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert';
|
|
|
3
3
|
import ManageListQuestion from '#src/components/manage-list/question.js';
|
|
4
4
|
import { ManageListSection } from '#src/components/manage-list/manage-list-section.js';
|
|
5
5
|
import { mockJourney } from '#test/mock/journey.js';
|
|
6
|
-
import { mockRandomUUID } from '#
|
|
6
|
+
import { mockRandomUUID } from '#test/mock/uuid.js';
|
|
7
7
|
import { configureNunjucksTestEnv } from '#test/utils/nunjucks.js';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { snapshotsDir } from '#test/utils/utils.js';
|
|
@@ -136,4 +136,41 @@ describe('components/manage-list/question', () => {
|
|
|
136
136
|
}
|
|
137
137
|
);
|
|
138
138
|
});
|
|
139
|
+
|
|
140
|
+
describe('getDataToSave', () => {
|
|
141
|
+
it('should return existing journey answers if present', async () => {
|
|
142
|
+
const q = newQuestion();
|
|
143
|
+
const existingData = [{ id: '1', name: 'Test' }];
|
|
144
|
+
const req = { body: {} };
|
|
145
|
+
const journeyResponse = {
|
|
146
|
+
answers: {
|
|
147
|
+
[FIELDNAME]: existingData
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const result = await q.getDataToSave(req, journeyResponse);
|
|
152
|
+
|
|
153
|
+
assert.deepStrictEqual(result, {
|
|
154
|
+
answers: {
|
|
155
|
+
[FIELDNAME]: existingData
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should return an empty array if no journey answers are present', async () => {
|
|
161
|
+
const q = newQuestion();
|
|
162
|
+
const req = { body: {} };
|
|
163
|
+
const journeyResponse = {
|
|
164
|
+
answers: {}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const result = await q.getDataToSave(req, journeyResponse);
|
|
168
|
+
|
|
169
|
+
assert.deepStrictEqual(result, {
|
|
170
|
+
answers: {
|
|
171
|
+
[FIELDNAME]: []
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
139
176
|
});
|
|
@@ -12,3 +12,24 @@ export function answerObjectForManageList(response, manageListQuestion, manageLi
|
|
|
12
12
|
}
|
|
13
13
|
return answers.find((a) => a.id === manageListItemId) || {};
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Similar to answerObjectForManageList but will edit response and add a new array entry if not found
|
|
18
|
+
*
|
|
19
|
+
* @param {import('#src/journey/journey-response.js').JourneyResponse} response
|
|
20
|
+
* @param {import('./question.js')} manageListQuestion
|
|
21
|
+
* @param {import('#src/journey/journey-types.d.ts').RouteParams} params
|
|
22
|
+
* @returns {import('#src/journey/journey-types.d.ts').ManageListAnswers}
|
|
23
|
+
*/
|
|
24
|
+
export function answerObjectForManageListSaving(response, manageListQuestion, params) {
|
|
25
|
+
const answersList =
|
|
26
|
+
response.answers[manageListQuestion?.fieldName] || (response.answers[manageListQuestion?.fieldName] = []);
|
|
27
|
+
let answers = answersList.find((item) => item.id === params.manageListItemId);
|
|
28
|
+
if (!answers) {
|
|
29
|
+
answers = {
|
|
30
|
+
id: params.manageListItemId
|
|
31
|
+
};
|
|
32
|
+
answersList.push(answers);
|
|
33
|
+
}
|
|
34
|
+
return answers;
|
|
35
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import { answerObjectForManageList } from '#src/components/manage-list/utils.js';
|
|
3
|
+
import { answerObjectForManageList, answerObjectForManageListSaving } from '#src/components/manage-list/utils.js';
|
|
4
4
|
|
|
5
5
|
describe('manage-list-utils', () => {
|
|
6
6
|
describe('answerObjectForManageList', () => {
|
|
@@ -44,4 +44,37 @@ describe('manage-list-utils', () => {
|
|
|
44
44
|
assert.deepStrictEqual(got, {});
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
describe('answerObjectForManageListSaving', () => {
|
|
49
|
+
it('should add an array with a new object if not found', () => {
|
|
50
|
+
const manageListQuestion = {
|
|
51
|
+
fieldName: 'manageListQuestion'
|
|
52
|
+
};
|
|
53
|
+
const journeyResponse = {
|
|
54
|
+
answers: {}
|
|
55
|
+
};
|
|
56
|
+
const params = { manageListItemId: '1' };
|
|
57
|
+
const got = answerObjectForManageListSaving(journeyResponse, manageListQuestion, params);
|
|
58
|
+
assert.deepStrictEqual(got, { id: '1' });
|
|
59
|
+
assert.ok(Array.isArray(journeyResponse.answers.manageListQuestion));
|
|
60
|
+
assert.ok(journeyResponse.answers.manageListQuestion[0]?.id, '1');
|
|
61
|
+
});
|
|
62
|
+
it('should return the manage list item object for manage list questions', () => {
|
|
63
|
+
const manageListQuestion = {
|
|
64
|
+
fieldName: 'manageListQuestion'
|
|
65
|
+
};
|
|
66
|
+
const answers = {
|
|
67
|
+
id: '1',
|
|
68
|
+
myField: 'my-value'
|
|
69
|
+
};
|
|
70
|
+
const journeyResponse = {
|
|
71
|
+
answers: {
|
|
72
|
+
manageListQuestion: [answers]
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const params = { manageListItemId: '1' };
|
|
76
|
+
const got = answerObjectForManageListSaving(journeyResponse, manageListQuestion, params);
|
|
77
|
+
assert.strictEqual(got, answers);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
47
80
|
});
|
|
@@ -74,28 +74,23 @@ export default class MultiFieldInputQuestion extends Question {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
* side effect: modifies journeyResponse with the new answers
|
|
77
|
+
* Get the data to save from the request, returns an object of answers
|
|
79
78
|
* @param {import('express').Request} req
|
|
80
79
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
81
80
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
82
|
-
*/
|
|
81
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
83
82
|
async getDataToSave(req, journeyResponse) {
|
|
84
|
-
|
|
85
|
-
* @type {{ answers: Record<string, unknown> }}
|
|
86
|
-
*/
|
|
87
|
-
let responseToSave = { answers: {} };
|
|
83
|
+
const answers = {};
|
|
88
84
|
|
|
89
85
|
for (const inputField of this.inputFields) {
|
|
90
86
|
let value = req.body[inputField.fieldName];
|
|
91
87
|
if (typeof value === 'string') {
|
|
92
88
|
value = value.trim();
|
|
93
89
|
}
|
|
94
|
-
|
|
95
|
-
journeyResponse.answers[inputField.fieldName] = responseToSave.answers[inputField.fieldName];
|
|
90
|
+
answers[inputField.fieldName] = value;
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
return
|
|
93
|
+
return { answers };
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
/**
|
|
@@ -123,7 +118,8 @@ export default class MultiFieldInputQuestion extends Question {
|
|
|
123
118
|
return [
|
|
124
119
|
{
|
|
125
120
|
key: `${this.title}`,
|
|
126
|
-
|
|
121
|
+
// Do not convert new lines to breaks in ManageListSection, as this causes a "doubled up" <br>
|
|
122
|
+
value: this.isInManageListSection ? escape(formattedAnswer) : nl2br(escape(formattedAnswer)),
|
|
127
123
|
action: this.getAction(sectionSegment, journey, summaryDetails)
|
|
128
124
|
}
|
|
129
125
|
];
|
|
@@ -34,7 +34,7 @@ function createMultiFieldInputQuestion(
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
describe('./src/dynamic-forms/components/
|
|
37
|
+
describe('./src/dynamic-forms/components/multi-field-input/question.js', () => {
|
|
38
38
|
it('should create', () => {
|
|
39
39
|
const testQuestion = createMultiFieldInputQuestion();
|
|
40
40
|
|
|
@@ -320,5 +320,33 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
|
|
|
320
320
|
|
|
321
321
|
assert.deepStrictEqual(result, expectedResult);
|
|
322
322
|
});
|
|
323
|
+
it('should return escaped text with newlines (no <br>) when isInManageListSection is true', () => {
|
|
324
|
+
const question = createMultiFieldInputQuestion();
|
|
325
|
+
|
|
326
|
+
question.isInManageListSection = true;
|
|
327
|
+
|
|
328
|
+
const journey = new Journey({
|
|
329
|
+
journeyId: 'TEST',
|
|
330
|
+
makeBaseUrl: () => 'base',
|
|
331
|
+
taskListUrl: 'cases/create-a-case/check-your-answers',
|
|
332
|
+
response: {
|
|
333
|
+
answers: {
|
|
334
|
+
testField1: 'Line 1',
|
|
335
|
+
testField2: 'Line & 2'
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
journeyTemplate: 'mock template',
|
|
339
|
+
taskListTemplate: 'mock path',
|
|
340
|
+
journeyTitle: 'mock title',
|
|
341
|
+
sections: []
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const expectedValue = 'Line 1\nLine & 2\n';
|
|
345
|
+
|
|
346
|
+
const result = question.formatAnswerForSummary('questions', journey);
|
|
347
|
+
|
|
348
|
+
assert.strictEqual(result[0].value, expectedValue);
|
|
349
|
+
assert.strictEqual(result[0].value.includes('<br>'), false);
|
|
350
|
+
});
|
|
323
351
|
});
|
|
324
352
|
});
|
|
@@ -60,10 +60,9 @@ export default class TextEntryRedactQuestion extends Question {
|
|
|
60
60
|
async getDataToSave(req, journeyResponse) {
|
|
61
61
|
if (this.useRedactedFieldNameForSave) {
|
|
62
62
|
const fieldName = this.fieldName + 'Redacted';
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return responseToSave;
|
|
63
|
+
const answers = {};
|
|
64
|
+
answers[fieldName] = req.body[this.fieldName];
|
|
65
|
+
return { answers };
|
|
67
66
|
}
|
|
68
67
|
return super.getDataToSave(req, journeyResponse);
|
|
69
68
|
}
|
|
@@ -127,17 +127,13 @@ export default class UnitOptionEntryQuestion extends Question {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
*
|
|
131
|
-
* side effect: modifies journeyResponse with the new answers
|
|
130
|
+
* Get the data to save from the request, returns an object of answers
|
|
132
131
|
* @param {import('express').Request} req
|
|
133
132
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
134
133
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
135
|
-
*/
|
|
134
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
136
135
|
async getDataToSave(req, journeyResponse) {
|
|
137
|
-
|
|
138
|
-
* @type {{ answers: Record<string, unknown> }}
|
|
139
|
-
*/
|
|
140
|
-
let responseToSave = { answers: {} };
|
|
136
|
+
const answers = {};
|
|
141
137
|
|
|
142
138
|
/** @type {string[]} */
|
|
143
139
|
const fields = Array.isArray(req.body[this.fieldName]) ? req.body[this.fieldName] : [req.body[this.fieldName]];
|
|
@@ -150,8 +146,7 @@ export default class UnitOptionEntryQuestion extends Question {
|
|
|
150
146
|
if (!selectedOptions.length)
|
|
151
147
|
throw new Error(`User submitted option(s) did not correlate with valid answers to ${this.fieldName} question`);
|
|
152
148
|
|
|
153
|
-
|
|
154
|
-
journeyResponse.answers[this.fieldName] = fieldValues;
|
|
149
|
+
answers[this.fieldName] = fieldValues.join(this.optionJoinString);
|
|
155
150
|
|
|
156
151
|
this.options.forEach((option) => {
|
|
157
152
|
if (!option.conditional) return;
|
|
@@ -162,13 +157,11 @@ export default class UnitOptionEntryQuestion extends Question {
|
|
|
162
157
|
if (optionIsSelectedOption) {
|
|
163
158
|
if (conditionalIsJustHTML(option.conditional)) return;
|
|
164
159
|
const conversionFactor = option.conditional.conversionFactor || 1;
|
|
165
|
-
|
|
166
|
-
responseToSave.answers[this.conditionalFieldName] = value;
|
|
167
|
-
journeyResponse.answers[this.conditionalFieldName] = value;
|
|
160
|
+
answers[this.conditionalFieldName] = req.body[option.conditional.fieldName] * conversionFactor;
|
|
168
161
|
}
|
|
169
162
|
});
|
|
170
163
|
|
|
171
|
-
return
|
|
164
|
+
return { answers };
|
|
172
165
|
}
|
|
173
166
|
|
|
174
167
|
/**
|
package/src/controller.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { SECTION_STATUS } from './section.js';
|
|
2
2
|
import questionUtils from './components/utils/question-utils.js';
|
|
3
|
+
import { answerObjectForManageListSaving } from '#src/components/manage-list/utils.js';
|
|
4
|
+
import { booleanToYesNoValue } from '#src/components/boolean/question.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* @typedef {import('./journey/journey.js').Journey} Journey
|
|
@@ -249,6 +251,22 @@ export function buildSave(saveData, redirectToTaskListOnSuccess) {
|
|
|
249
251
|
if (redirectToTaskListOnSuccess) {
|
|
250
252
|
return res.redirect(journey.taskListUrl);
|
|
251
253
|
}
|
|
254
|
+
// edit the journey.response which question.shouldDisplay uses
|
|
255
|
+
// we need to ensure the latest answer just submitted is included
|
|
256
|
+
// as question.shouldDisplay checks the response and is used to determine the next question
|
|
257
|
+
let answers = journeyResponse.answers;
|
|
258
|
+
if (question.isInManageListSection) {
|
|
259
|
+
answers = answerObjectForManageListSaving(journeyResponse, manageListQuestion, req.params);
|
|
260
|
+
}
|
|
261
|
+
for (const [k, v] of Object.entries(data?.answers || {})) {
|
|
262
|
+
if (typeof v === 'boolean') {
|
|
263
|
+
// boolean answers are saved at booleans, but stored in session and
|
|
264
|
+
// checked by display logic as yes/no
|
|
265
|
+
answers[k] = booleanToYesNoValue(v);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
answers[k] = v;
|
|
269
|
+
}
|
|
252
270
|
// move to the next question
|
|
253
271
|
return journey.redirectToNextQuestion(res, req.params, manageListQuestion);
|
|
254
272
|
} catch (err) {
|
package/src/controller.test.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Journey } from './journey/journey.js';
|
|
|
6
6
|
import { SECTION_STATUS } from './section.js';
|
|
7
7
|
|
|
8
8
|
import { mockReq, mockRes } from '#test/utils/utils.js';
|
|
9
|
+
import { BOOLEAN_OPTIONS } from '#src/components/boolean/question.js';
|
|
9
10
|
|
|
10
11
|
const res = mockRes();
|
|
11
12
|
const mockBaseUrl = '/manage-appeals/questionnaire';
|
|
@@ -379,6 +380,7 @@ describe('dynamic-form/controller', () => {
|
|
|
379
380
|
const saveData = mock.fn();
|
|
380
381
|
await buildSave(saveData)(req, res, journeyId);
|
|
381
382
|
|
|
383
|
+
assert.strictEqual(sampleQuestionObj.getDataToSave.mock.callCount(), 1);
|
|
382
384
|
assert.strictEqual(saveData.mock.callCount(), 1);
|
|
383
385
|
assert.deepStrictEqual(saveData.mock.calls[0].arguments, [
|
|
384
386
|
{
|
|
@@ -499,6 +501,12 @@ describe('dynamic-form/controller', () => {
|
|
|
499
501
|
const sampleQuestionObjWithActions = {
|
|
500
502
|
...sampleQuestionObj
|
|
501
503
|
};
|
|
504
|
+
sampleQuestionObjWithActions.getDataToSave.mock.mockImplementationOnce(() => ({
|
|
505
|
+
answers: {
|
|
506
|
+
[sampleQuestionObj.fieldName]: 'my-answer',
|
|
507
|
+
aBooleanToTest: true
|
|
508
|
+
}
|
|
509
|
+
}));
|
|
502
510
|
|
|
503
511
|
req.params = {
|
|
504
512
|
referenceId: mockRef,
|
|
@@ -522,6 +530,10 @@ describe('dynamic-form/controller', () => {
|
|
|
522
530
|
assert.strictEqual(saveData.mock.callCount(), 1);
|
|
523
531
|
assert.strictEqual(res.redirect.mock.callCount(), 1);
|
|
524
532
|
assert.strictEqual(res.redirect.mock.calls[0].arguments[0], expectedUrl);
|
|
533
|
+
// journey response should be edited if redirecting to next question
|
|
534
|
+
assert.strictEqual(res.locals.journeyResponse.answers?.sampleFieldName, 'my-answer');
|
|
535
|
+
// booleans must be strings in journey response
|
|
536
|
+
assert.strictEqual(res.locals.journeyResponse.answers?.aBooleanToTest, BOOLEAN_OPTIONS.YES);
|
|
525
537
|
});
|
|
526
538
|
|
|
527
539
|
it('should redirect to task list if configured', async () => {
|
|
@@ -531,6 +543,9 @@ describe('dynamic-form/controller', () => {
|
|
|
531
543
|
const sampleQuestionObjWithActions = {
|
|
532
544
|
...sampleQuestionObj
|
|
533
545
|
};
|
|
546
|
+
sampleQuestionObjWithActions.getDataToSave.mock.mockImplementationOnce(() => ({
|
|
547
|
+
answers: { [sampleQuestionObj.fieldName]: 'my-answer' }
|
|
548
|
+
}));
|
|
534
549
|
|
|
535
550
|
req.params = {
|
|
536
551
|
referenceId: mockRef,
|
|
@@ -555,6 +570,8 @@ describe('dynamic-form/controller', () => {
|
|
|
555
570
|
assert.strictEqual(res.redirect.mock.callCount(), 1);
|
|
556
571
|
assert.match(res.redirect.mock.calls[0].arguments[0], /\/task-list$/);
|
|
557
572
|
assert.strictEqual(mockJourney.getNextQuestionUrl.mock.callCount(), 0);
|
|
573
|
+
// journey response should not be edited if redirecting to task list
|
|
574
|
+
assert.strictEqual(Object.keys(res.locals.journeyResponse.answers).length, 0);
|
|
558
575
|
});
|
|
559
576
|
|
|
560
577
|
describe('manageListQuestions', () => {
|
|
@@ -585,7 +602,8 @@ describe('dynamic-form/controller', () => {
|
|
|
585
602
|
const journey = {
|
|
586
603
|
getSection: mock.fn(() => ({ segment: 'section-1' })),
|
|
587
604
|
getQuestionByParams: mock.fn(),
|
|
588
|
-
redirectToNextQuestion: mock.fn()
|
|
605
|
+
redirectToNextQuestion: mock.fn(),
|
|
606
|
+
response: { answers: {} }
|
|
589
607
|
};
|
|
590
608
|
const res = {
|
|
591
609
|
locals: {
|
package/src/lib/uuid.test.js
CHANGED
|
@@ -2,18 +2,6 @@ import { Uuid } from '#src/lib/uuid.js';
|
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import assert from 'node:assert';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
* @param {import('node:test').TestContext} ctx
|
|
8
|
-
* @param {string} [uuid]
|
|
9
|
-
*/
|
|
10
|
-
export function mockRandomUUID(ctx, uuid = '00000000-0000-0000-0000-000000000000') {
|
|
11
|
-
const original = Uuid.randomUUID;
|
|
12
|
-
Uuid.randomUUID = ctx.mock.fn(() => uuid);
|
|
13
|
-
// restore after the tests
|
|
14
|
-
ctx.after(() => (Uuid.randomUUID = original));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
5
|
describe('uuid', () => {
|
|
18
6
|
it('should return a valid v4 UUID', () => {
|
|
19
7
|
assert.match(
|
|
@@ -120,17 +120,13 @@ export default class OptionsQuestion extends Question {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
*
|
|
124
|
-
* side effect: modifies journeyResponse with the new answers
|
|
123
|
+
* Get the data to save from the request, returns an object of answers
|
|
125
124
|
* @param {import('express').Request} req
|
|
126
125
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
127
126
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
128
|
-
*/
|
|
127
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
129
128
|
async getDataToSave(req, journeyResponse) {
|
|
130
|
-
|
|
131
|
-
* @type {{ answers: Record<string, unknown> }}
|
|
132
|
-
*/
|
|
133
|
-
let responseToSave = { answers: {} };
|
|
129
|
+
const answers = {};
|
|
134
130
|
|
|
135
131
|
const fields = Array.isArray(req.body[this.fieldName]) ? req.body[this.fieldName] : [req.body[this.fieldName]];
|
|
136
132
|
const fieldValues = fields.map((x) => x.trim());
|
|
@@ -142,8 +138,7 @@ export default class OptionsQuestion extends Question {
|
|
|
142
138
|
if (!selectedOptions.length)
|
|
143
139
|
throw new Error(`User submitted option(s) did not correlate with valid answers to ${this.fieldName} question`);
|
|
144
140
|
|
|
145
|
-
|
|
146
|
-
journeyResponse.answers[this.fieldName] = fieldValues;
|
|
141
|
+
answers[this.fieldName] = fieldValues.join(this.optionJoinString);
|
|
147
142
|
|
|
148
143
|
this.options.forEach((option) => {
|
|
149
144
|
if (!option.conditional) return;
|
|
@@ -152,11 +147,9 @@ export default class OptionsQuestion extends Question {
|
|
|
152
147
|
(selectedOption) => option.text === selectedOption.text && option.value === selectedOption.value
|
|
153
148
|
);
|
|
154
149
|
|
|
155
|
-
|
|
156
|
-
responseToSave.answers[key] = value;
|
|
157
|
-
journeyResponse.answers[key] = value;
|
|
150
|
+
answers[key] = optionIsSelectedOption ? req.body[key]?.trim() : null;
|
|
158
151
|
});
|
|
159
152
|
|
|
160
|
-
return
|
|
153
|
+
return { answers };
|
|
161
154
|
}
|
|
162
155
|
}
|
|
@@ -338,38 +338,24 @@ export class Question {
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
341
|
+
* Get the data to save from the request, returns an object of answers
|
|
342
|
+
*
|
|
343
343
|
* @param {import('express').Request} req
|
|
344
|
-
* @param {JourneyResponse} journeyResponse - current journey response
|
|
344
|
+
* @param {JourneyResponse} journeyResponse - current journey response
|
|
345
345
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
346
|
-
*/
|
|
346
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
347
347
|
async getDataToSave(req, journeyResponse) {
|
|
348
|
-
|
|
349
|
-
* @type {{ answers: Record<string, unknown> }}
|
|
350
|
-
*/
|
|
351
|
-
let responseToSave = { answers: {} };
|
|
348
|
+
const answers = {};
|
|
352
349
|
|
|
353
|
-
|
|
350
|
+
answers[this.fieldName] = req.body[this.fieldName];
|
|
354
351
|
|
|
355
352
|
for (const propName in req.body) {
|
|
356
353
|
if (propName.startsWith(this.fieldName + '_')) {
|
|
357
|
-
|
|
358
|
-
journeyResponse.answers[propName] = req.body[propName];
|
|
354
|
+
answers[propName] = req.body[propName];
|
|
359
355
|
}
|
|
360
|
-
// todo: sort this
|
|
361
|
-
// } else if (numericFields.has(propName)) {
|
|
362
|
-
// const numericValue = Number(req.body[propName]);
|
|
363
|
-
// if (!isNaN(numericValue)) {
|
|
364
|
-
// responseToSave.answers[propName] = numericValue;
|
|
365
|
-
// journeyResponse.answers[propName] = numericValue;
|
|
366
|
-
// }
|
|
367
|
-
// }
|
|
368
356
|
}
|
|
369
357
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return responseToSave;
|
|
358
|
+
return { answers };
|
|
373
359
|
}
|
|
374
360
|
|
|
375
361
|
/**
|
|
@@ -325,7 +325,7 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
325
325
|
});
|
|
326
326
|
|
|
327
327
|
describe('getDataToSave', () => {
|
|
328
|
-
it('should return answer from req.body
|
|
328
|
+
it('should return answer from req.body', async () => {
|
|
329
329
|
const question = getTestQuestion();
|
|
330
330
|
|
|
331
331
|
const req = {
|
|
@@ -333,14 +333,8 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
333
333
|
[question.fieldName]: { a: 1 }
|
|
334
334
|
}
|
|
335
335
|
};
|
|
336
|
-
const journeyResponse = {
|
|
337
|
-
answers: {
|
|
338
|
-
[question.fieldName]: { b: 1 },
|
|
339
|
-
other: 'another-answer'
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
336
|
|
|
343
|
-
const result = await question.getDataToSave(req
|
|
337
|
+
const result = await question.getDataToSave(req);
|
|
344
338
|
|
|
345
339
|
const expectedResult = {
|
|
346
340
|
answers: {
|
|
@@ -348,8 +342,6 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
348
342
|
}
|
|
349
343
|
};
|
|
350
344
|
assert.deepStrictEqual(result, expectedResult);
|
|
351
|
-
expectedResult.answers.other = 'another-answer';
|
|
352
|
-
assert.deepStrictEqual(journeyResponse, expectedResult);
|
|
353
345
|
});
|
|
354
346
|
|
|
355
347
|
it('should handle nested properties', async () => {
|
|
@@ -362,14 +354,8 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
362
354
|
[question.fieldName + '_2']: { a: 3 }
|
|
363
355
|
}
|
|
364
356
|
};
|
|
365
|
-
const journeyResponse = {
|
|
366
|
-
answers: {
|
|
367
|
-
[question.fieldName]: { b: 1 },
|
|
368
|
-
other: 'another-answer'
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
357
|
|
|
372
|
-
const result = await question.getDataToSave(req
|
|
358
|
+
const result = await question.getDataToSave(req);
|
|
373
359
|
|
|
374
360
|
const expectedResult = {
|
|
375
361
|
answers: {
|
|
@@ -379,8 +365,6 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
379
365
|
}
|
|
380
366
|
};
|
|
381
367
|
assert.deepStrictEqual(result, expectedResult);
|
|
382
|
-
expectedResult.answers.other = 'another-answer';
|
|
383
|
-
assert.deepStrictEqual(journeyResponse, expectedResult);
|
|
384
368
|
});
|
|
385
369
|
});
|
|
386
370
|
|
|
@@ -3,7 +3,7 @@ import assert from 'assert';
|
|
|
3
3
|
import { manageListQuestions, questionsInOrder } from './questions.js';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { escapeForRegExp, snapshotsDir } from './utils/utils.js';
|
|
6
|
-
import { createAppWithQuestions, mockAnswer, mockAnswerBody } from './
|
|
6
|
+
import { createAppWithQuestions, mockAnswer, mockAnswerBody } from './utils/question-test-utils.js';
|
|
7
7
|
import { COMPONENT_TYPES } from '#src/index.js';
|
|
8
8
|
|
|
9
9
|
describe('check-your-answers', () => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Uuid } from '#src/lib/uuid.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param {import('node:test').TestContext} ctx
|
|
6
|
+
* @param {string} [uuid]
|
|
7
|
+
*/
|
|
8
|
+
export function mockRandomUUID(ctx, uuid = '00000000-0000-0000-0000-000000000000') {
|
|
9
|
+
const original = Uuid.randomUUID;
|
|
10
|
+
Uuid.randomUUID = ctx.mock.fn(() => uuid);
|
|
11
|
+
// restore after the tests
|
|
12
|
+
ctx.after(() => (Uuid.randomUUID = original));
|
|
13
|
+
}
|
package/test/questions.test.js
CHANGED
|
@@ -1,76 +1,11 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'assert';
|
|
3
|
-
import {
|
|
4
|
-
import { TestServer } from './utils/test-server.js';
|
|
5
|
-
import { getQuestions, manageListQuestions, questionsInOrder } from './questions.js';
|
|
6
|
-
import { buildGetJourney } from '../src/middleware/build-get-journey.js';
|
|
7
|
-
import { createJourney, JOURNEY_ID } from './journey.js';
|
|
8
|
-
import { buildGetJourneyResponseFromSession, saveDataToSession } from '../src/lib/session-answer-store.js';
|
|
9
|
-
import validate from '../src/validator/validator.js';
|
|
10
|
-
import { validationErrorHandler } from '../src/validator/validation-error-handler.js';
|
|
11
|
-
import { buildSave, list, question } from '../src/controller.js';
|
|
3
|
+
import { manageListQuestions, questionsInOrder } from './questions.js';
|
|
12
4
|
import path from 'path';
|
|
13
5
|
import { COMPONENT_TYPES } from '../src/index.js';
|
|
14
6
|
import { escapeForRegExp, snapshotsDir } from './utils/utils.js';
|
|
15
|
-
import {
|
|
16
|
-
import { mockRandomUUID } from '#
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {import('node:test').TestContext} ctx
|
|
20
|
-
* @returns {Promise<TestServer>}
|
|
21
|
-
*/
|
|
22
|
-
export async function createAppWithQuestions(ctx) {
|
|
23
|
-
const app = createApp();
|
|
24
|
-
const questions = getQuestions();
|
|
25
|
-
const getJourney = buildGetJourney((req, journeyResponse) => createJourney(questions, journeyResponse));
|
|
26
|
-
const getJourneyResponse = buildGetJourneyResponseFromSession(JOURNEY_ID);
|
|
27
|
-
|
|
28
|
-
// app.use((req, res, next) => {
|
|
29
|
-
// console.log(req.method, req.url, req.session);
|
|
30
|
-
// next();
|
|
31
|
-
// });
|
|
32
|
-
|
|
33
|
-
app.get(
|
|
34
|
-
'/:section/:question{/:manageListAction/:manageListItemId/:manageListQuestion}',
|
|
35
|
-
getJourneyResponse,
|
|
36
|
-
getJourney,
|
|
37
|
-
question
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
app.post(
|
|
41
|
-
'/:section/:question{/:manageListAction/:manageListItemId/:manageListQuestion}',
|
|
42
|
-
getJourneyResponse,
|
|
43
|
-
getJourney,
|
|
44
|
-
validate,
|
|
45
|
-
validationErrorHandler,
|
|
46
|
-
buildSave(saveDataToSession)
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
app.get('/check-your-answers', getJourneyResponse, getJourney, (req, res) => list(req, res, '', {}));
|
|
50
|
-
|
|
51
|
-
app.use((req, res) => {
|
|
52
|
-
res.status(404);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* @type {import('express').ErrorRequestHandler}
|
|
57
|
-
*/
|
|
58
|
-
function errorHandler(error, req, res, next) {
|
|
59
|
-
console.error('Internal Server Error:', error);
|
|
60
|
-
if (res.headersSent) {
|
|
61
|
-
return next(error);
|
|
62
|
-
}
|
|
63
|
-
res.status(500);
|
|
64
|
-
res.render('error', { error });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
app.use(errorHandler);
|
|
68
|
-
|
|
69
|
-
const server = new TestServer(app, { rememberCookies: true, timeoutMs: 2000 });
|
|
70
|
-
await server.start();
|
|
71
|
-
ctx.after(async () => await server.stop());
|
|
72
|
-
return server;
|
|
73
|
-
}
|
|
7
|
+
import { createAppWithQuestions, mockAnswerBody } from '#test/utils/question-test-utils.js';
|
|
8
|
+
import { mockRandomUUID } from '#test/mock/uuid.js';
|
|
74
9
|
|
|
75
10
|
describe('question pages', () => {
|
|
76
11
|
it('should have a test for each question type', () => {
|
|
@@ -187,135 +122,3 @@ describe('question pages', () => {
|
|
|
187
122
|
}
|
|
188
123
|
});
|
|
189
124
|
});
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Build a valid payload for a question.
|
|
193
|
-
* @param {import('../src/questions/question-props.js').QuestionProps} q
|
|
194
|
-
* @returns {Object}
|
|
195
|
-
*/
|
|
196
|
-
export function mockAnswerBody(q) {
|
|
197
|
-
switch (q.type) {
|
|
198
|
-
case COMPONENT_TYPES.BOOLEAN:
|
|
199
|
-
return { [q.fieldName]: BOOLEAN_OPTIONS.YES };
|
|
200
|
-
case COMPONENT_TYPES.CHECKBOX:
|
|
201
|
-
return { [q.fieldName]: [q.options[0].value] };
|
|
202
|
-
case COMPONENT_TYPES.RADIO:
|
|
203
|
-
case COMPONENT_TYPES.SELECT:
|
|
204
|
-
return { [q.fieldName]: q.options[0].value };
|
|
205
|
-
case COMPONENT_TYPES.NUMBER:
|
|
206
|
-
return { [q.fieldName]: 1 };
|
|
207
|
-
case COMPONENT_TYPES.DATE:
|
|
208
|
-
return {
|
|
209
|
-
[q.fieldName + '_day']: 1,
|
|
210
|
-
[q.fieldName + '_month']: 1,
|
|
211
|
-
[q.fieldName + '_year']: 2025
|
|
212
|
-
};
|
|
213
|
-
case COMPONENT_TYPES.DATE_PERIOD:
|
|
214
|
-
return {
|
|
215
|
-
[q.fieldName + '_start_day']: 1,
|
|
216
|
-
[q.fieldName + '_start_month']: 1,
|
|
217
|
-
[q.fieldName + '_start_year']: 2025,
|
|
218
|
-
[q.fieldName + '_end_day']: 2,
|
|
219
|
-
[q.fieldName + '_end_month']: 1,
|
|
220
|
-
[q.fieldName + '_end_year']: 2025
|
|
221
|
-
};
|
|
222
|
-
case COMPONENT_TYPES.DATE_TIME:
|
|
223
|
-
return {
|
|
224
|
-
[q.fieldName + '_day']: 1,
|
|
225
|
-
[q.fieldName + '_month']: 1,
|
|
226
|
-
[q.fieldName + '_year']: 2025,
|
|
227
|
-
[q.fieldName + '_hour']: 10,
|
|
228
|
-
[q.fieldName + '_minutes']: 45,
|
|
229
|
-
[q.fieldName + '_period']: 'am'
|
|
230
|
-
};
|
|
231
|
-
case COMPONENT_TYPES.TEXT_ENTRY:
|
|
232
|
-
case COMPONENT_TYPES.SINGLE_LINE_INPUT:
|
|
233
|
-
return { [q.fieldName]: 'test' };
|
|
234
|
-
case COMPONENT_TYPES.TEXT_ENTRY_REDACT:
|
|
235
|
-
return { [q.fieldName]: 'secret' };
|
|
236
|
-
case COMPONENT_TYPES.MULTI_FIELD_INPUT: {
|
|
237
|
-
const res = {};
|
|
238
|
-
for (const field of q.inputFields) {
|
|
239
|
-
res[field.fieldName] = mockAnswer(field);
|
|
240
|
-
}
|
|
241
|
-
return res;
|
|
242
|
-
}
|
|
243
|
-
case COMPONENT_TYPES.ADDRESS:
|
|
244
|
-
return {
|
|
245
|
-
[q.fieldName + '_addressLine1']: '1 Test St',
|
|
246
|
-
[q.fieldName + '_townCity']: 'Testville',
|
|
247
|
-
[q.fieldName + '_postcode']: 'TE5 5ST'
|
|
248
|
-
};
|
|
249
|
-
case COMPONENT_TYPES.UNIT_OPTION:
|
|
250
|
-
return {
|
|
251
|
-
[q.fieldName]: 'kg',
|
|
252
|
-
[q.options[0].conditional.fieldName]: 10
|
|
253
|
-
};
|
|
254
|
-
case COMPONENT_TYPES.EMAIL:
|
|
255
|
-
return { [q.fieldName]: 'test@example.com' };
|
|
256
|
-
case COMPONENT_TYPES.MANAGE_LIST: {
|
|
257
|
-
// manage list questions not available here so just hard coded
|
|
258
|
-
return {
|
|
259
|
-
[q.fieldName]: [
|
|
260
|
-
{ id: '1234', travelCompanionName: 'Companion 1', travelCompanionEmail: 'companion-1@example.com' },
|
|
261
|
-
{ id: '4567', travelCompanionName: 'Companion 2', travelCompanionEmail: 'companion-2@example.com' },
|
|
262
|
-
{ id: '8901', travelCompanionName: 'Companion 3', travelCompanionEmail: 'companion-3@example.com' }
|
|
263
|
-
]
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
default:
|
|
267
|
-
return { [q.fieldName]: 'test' };
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Build a valid & formatted answer for a question.
|
|
273
|
-
* @param {import('../src/questions/question-props.js').QuestionProps} q
|
|
274
|
-
* @returns {Object}
|
|
275
|
-
*/
|
|
276
|
-
export function mockAnswer(q) {
|
|
277
|
-
switch (q.type) {
|
|
278
|
-
case COMPONENT_TYPES.BOOLEAN:
|
|
279
|
-
return BOOLEAN_OPTIONS.YES;
|
|
280
|
-
case COMPONENT_TYPES.CHECKBOX:
|
|
281
|
-
return q.options[0].text;
|
|
282
|
-
case COMPONENT_TYPES.RADIO:
|
|
283
|
-
case COMPONENT_TYPES.SELECT:
|
|
284
|
-
return q.options[0].text;
|
|
285
|
-
case COMPONENT_TYPES.NUMBER:
|
|
286
|
-
return 1;
|
|
287
|
-
case COMPONENT_TYPES.DATE:
|
|
288
|
-
return '1 January 2025';
|
|
289
|
-
case COMPONENT_TYPES.DATE_PERIOD:
|
|
290
|
-
return 'Start: 00:00 1 January 2025<br>End: 00:00 2 January 2025';
|
|
291
|
-
case COMPONENT_TYPES.DATE_TIME:
|
|
292
|
-
return '1 January 2025<br>10:45am';
|
|
293
|
-
case COMPONENT_TYPES.TEXT_ENTRY:
|
|
294
|
-
case COMPONENT_TYPES.SINGLE_LINE_INPUT:
|
|
295
|
-
return 'test';
|
|
296
|
-
case COMPONENT_TYPES.TEXT_ENTRY_REDACT:
|
|
297
|
-
return 'secret';
|
|
298
|
-
case COMPONENT_TYPES.MULTI_FIELD_INPUT:
|
|
299
|
-
if (q.inputFields) {
|
|
300
|
-
let res = '';
|
|
301
|
-
for (const field of q.inputFields) {
|
|
302
|
-
res += mockAnswer(field) + '<br>';
|
|
303
|
-
}
|
|
304
|
-
return res;
|
|
305
|
-
}
|
|
306
|
-
break;
|
|
307
|
-
case COMPONENT_TYPES.ADDRESS: {
|
|
308
|
-
const answer = mockAnswerBody(q);
|
|
309
|
-
return Object.values(answer).filter(Boolean).join('<br>');
|
|
310
|
-
}
|
|
311
|
-
case COMPONENT_TYPES.UNIT_OPTION:
|
|
312
|
-
return '10 kg';
|
|
313
|
-
case COMPONENT_TYPES.EMAIL:
|
|
314
|
-
return 'test@example.com';
|
|
315
|
-
case COMPONENT_TYPES.MANAGE_LIST: {
|
|
316
|
-
return `3 ${q.title}`;
|
|
317
|
-
}
|
|
318
|
-
default:
|
|
319
|
-
return 'test';
|
|
320
|
-
}
|
|
321
|
-
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { getQuestions } from '#test/questions.js';
|
|
2
|
+
import { COMPONENT_TYPES } from '#src/index.js';
|
|
3
|
+
import { createApp } from '#test/utils/app.js';
|
|
4
|
+
import { buildGetJourney } from '#src/middleware/build-get-journey.js';
|
|
5
|
+
import { buildGetJourneyResponseFromSession, saveDataToSession } from '#src/lib/session-answer-store.js';
|
|
6
|
+
import { createJourney, JOURNEY_ID } from '#test/journey.js';
|
|
7
|
+
import { buildList, buildSave, question } from '#src/controller.js';
|
|
8
|
+
import validate from '#src/validator/validator.js';
|
|
9
|
+
import { validationErrorHandler } from '#src/validator/validation-error-handler.js';
|
|
10
|
+
import { TestServer } from '#test/utils/test-server.js';
|
|
11
|
+
import { BOOLEAN_OPTIONS } from '#src/components/boolean/question.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('node:test').TestContext} ctx
|
|
15
|
+
* @returns {Promise<TestServer>}
|
|
16
|
+
*/
|
|
17
|
+
export async function createAppWithQuestions(ctx) {
|
|
18
|
+
const app = createApp();
|
|
19
|
+
const questions = getQuestions();
|
|
20
|
+
const getJourney = buildGetJourney((req, journeyResponse) => createJourney(questions, journeyResponse));
|
|
21
|
+
const getJourneyResponse = buildGetJourneyResponseFromSession(JOURNEY_ID);
|
|
22
|
+
|
|
23
|
+
// app.use((req, res, next) => {
|
|
24
|
+
// console.log(req.method, req.url, req.session);
|
|
25
|
+
// next();
|
|
26
|
+
// });
|
|
27
|
+
|
|
28
|
+
app.get(
|
|
29
|
+
'/:section/:question{/:manageListAction/:manageListItemId/:manageListQuestion}',
|
|
30
|
+
getJourneyResponse,
|
|
31
|
+
getJourney,
|
|
32
|
+
question
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
app.post(
|
|
36
|
+
'/:section/:question{/:manageListAction/:manageListItemId/:manageListQuestion}',
|
|
37
|
+
getJourneyResponse,
|
|
38
|
+
getJourney,
|
|
39
|
+
validate,
|
|
40
|
+
validationErrorHandler,
|
|
41
|
+
buildSave(saveDataToSession)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
app.get('/check-your-answers', getJourneyResponse, getJourney, buildList());
|
|
45
|
+
|
|
46
|
+
app.use((req, res) => {
|
|
47
|
+
res.status(404);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @type {import('express').ErrorRequestHandler}
|
|
52
|
+
*/
|
|
53
|
+
function errorHandler(error, req, res, next) {
|
|
54
|
+
console.error('Internal Server Error:', error);
|
|
55
|
+
if (res.headersSent) {
|
|
56
|
+
return next(error);
|
|
57
|
+
}
|
|
58
|
+
res.status(500);
|
|
59
|
+
res.render('error', { error });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
app.use(errorHandler);
|
|
63
|
+
|
|
64
|
+
const server = new TestServer(app, { rememberCookies: true, timeoutMs: 2000 });
|
|
65
|
+
await server.start();
|
|
66
|
+
ctx.after(async () => await server.stop());
|
|
67
|
+
return server;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a valid payload for a question.
|
|
72
|
+
* @param {import('../src/questions/question-props.js').QuestionProps} q
|
|
73
|
+
* @returns {Object}
|
|
74
|
+
*/
|
|
75
|
+
export function mockAnswerBody(q) {
|
|
76
|
+
switch (q.type) {
|
|
77
|
+
case COMPONENT_TYPES.BOOLEAN:
|
|
78
|
+
return { [q.fieldName]: BOOLEAN_OPTIONS.YES };
|
|
79
|
+
case COMPONENT_TYPES.CHECKBOX:
|
|
80
|
+
return { [q.fieldName]: [q.options[0].value] };
|
|
81
|
+
case COMPONENT_TYPES.RADIO:
|
|
82
|
+
case COMPONENT_TYPES.SELECT:
|
|
83
|
+
return { [q.fieldName]: q.options[0].value };
|
|
84
|
+
case COMPONENT_TYPES.NUMBER:
|
|
85
|
+
return { [q.fieldName]: 1 };
|
|
86
|
+
case COMPONENT_TYPES.DATE:
|
|
87
|
+
return {
|
|
88
|
+
[q.fieldName + '_day']: 1,
|
|
89
|
+
[q.fieldName + '_month']: 1,
|
|
90
|
+
[q.fieldName + '_year']: 2025
|
|
91
|
+
};
|
|
92
|
+
case COMPONENT_TYPES.DATE_PERIOD:
|
|
93
|
+
return {
|
|
94
|
+
[q.fieldName + '_start_day']: 1,
|
|
95
|
+
[q.fieldName + '_start_month']: 1,
|
|
96
|
+
[q.fieldName + '_start_year']: 2025,
|
|
97
|
+
[q.fieldName + '_end_day']: 2,
|
|
98
|
+
[q.fieldName + '_end_month']: 1,
|
|
99
|
+
[q.fieldName + '_end_year']: 2025
|
|
100
|
+
};
|
|
101
|
+
case COMPONENT_TYPES.DATE_TIME:
|
|
102
|
+
return {
|
|
103
|
+
[q.fieldName + '_day']: 1,
|
|
104
|
+
[q.fieldName + '_month']: 1,
|
|
105
|
+
[q.fieldName + '_year']: 2025,
|
|
106
|
+
[q.fieldName + '_hour']: 10,
|
|
107
|
+
[q.fieldName + '_minutes']: 45,
|
|
108
|
+
[q.fieldName + '_period']: 'am'
|
|
109
|
+
};
|
|
110
|
+
case COMPONENT_TYPES.TEXT_ENTRY:
|
|
111
|
+
case COMPONENT_TYPES.SINGLE_LINE_INPUT:
|
|
112
|
+
return { [q.fieldName]: 'test' };
|
|
113
|
+
case COMPONENT_TYPES.TEXT_ENTRY_REDACT:
|
|
114
|
+
return { [q.fieldName]: 'secret' };
|
|
115
|
+
case COMPONENT_TYPES.MULTI_FIELD_INPUT: {
|
|
116
|
+
const res = {};
|
|
117
|
+
for (const field of q.inputFields) {
|
|
118
|
+
res[field.fieldName] = mockAnswer(field);
|
|
119
|
+
}
|
|
120
|
+
return res;
|
|
121
|
+
}
|
|
122
|
+
case COMPONENT_TYPES.ADDRESS:
|
|
123
|
+
return {
|
|
124
|
+
[q.fieldName + '_addressLine1']: '1 Test St',
|
|
125
|
+
[q.fieldName + '_townCity']: 'Testville',
|
|
126
|
+
[q.fieldName + '_postcode']: 'TE5 5ST'
|
|
127
|
+
};
|
|
128
|
+
case COMPONENT_TYPES.UNIT_OPTION:
|
|
129
|
+
return {
|
|
130
|
+
[q.fieldName]: 'kg',
|
|
131
|
+
[q.options[0].conditional.fieldName]: 10
|
|
132
|
+
};
|
|
133
|
+
case COMPONENT_TYPES.EMAIL:
|
|
134
|
+
return { [q.fieldName]: 'test@example.com' };
|
|
135
|
+
case COMPONENT_TYPES.MANAGE_LIST: {
|
|
136
|
+
// manage list questions not available here so just hard coded
|
|
137
|
+
return {
|
|
138
|
+
[q.fieldName]: [
|
|
139
|
+
{ id: '1234', travelCompanionName: 'Companion 1', travelCompanionEmail: 'companion-1@example.com' },
|
|
140
|
+
{ id: '4567', travelCompanionName: 'Companion 2', travelCompanionEmail: 'companion-2@example.com' },
|
|
141
|
+
{ id: '8901', travelCompanionName: 'Companion 3', travelCompanionEmail: 'companion-3@example.com' }
|
|
142
|
+
]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
return { [q.fieldName]: 'test' };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Build a valid & formatted answer for a question.
|
|
152
|
+
* @param {import('../src/questions/question-props.js').QuestionProps} q
|
|
153
|
+
* @returns {Object}
|
|
154
|
+
*/
|
|
155
|
+
export function mockAnswer(q) {
|
|
156
|
+
switch (q.type) {
|
|
157
|
+
case COMPONENT_TYPES.BOOLEAN:
|
|
158
|
+
return BOOLEAN_OPTIONS.YES;
|
|
159
|
+
case COMPONENT_TYPES.CHECKBOX:
|
|
160
|
+
return q.options[0].text;
|
|
161
|
+
case COMPONENT_TYPES.RADIO:
|
|
162
|
+
case COMPONENT_TYPES.SELECT:
|
|
163
|
+
return q.options[0].text;
|
|
164
|
+
case COMPONENT_TYPES.NUMBER:
|
|
165
|
+
return 1;
|
|
166
|
+
case COMPONENT_TYPES.DATE:
|
|
167
|
+
return '1 January 2025';
|
|
168
|
+
case COMPONENT_TYPES.DATE_PERIOD:
|
|
169
|
+
return 'Start: 00:00 1 January 2025<br>End: 00:00 2 January 2025';
|
|
170
|
+
case COMPONENT_TYPES.DATE_TIME:
|
|
171
|
+
return '1 January 2025<br>10:45am';
|
|
172
|
+
case COMPONENT_TYPES.TEXT_ENTRY:
|
|
173
|
+
case COMPONENT_TYPES.SINGLE_LINE_INPUT:
|
|
174
|
+
return 'test';
|
|
175
|
+
case COMPONENT_TYPES.TEXT_ENTRY_REDACT:
|
|
176
|
+
return 'secret';
|
|
177
|
+
case COMPONENT_TYPES.MULTI_FIELD_INPUT:
|
|
178
|
+
if (q.inputFields) {
|
|
179
|
+
let res = '';
|
|
180
|
+
for (const field of q.inputFields) {
|
|
181
|
+
res += mockAnswer(field) + '<br>';
|
|
182
|
+
}
|
|
183
|
+
return res;
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
case COMPONENT_TYPES.ADDRESS: {
|
|
187
|
+
const answer = mockAnswerBody(q);
|
|
188
|
+
return Object.values(answer).filter(Boolean).join('<br>');
|
|
189
|
+
}
|
|
190
|
+
case COMPONENT_TYPES.UNIT_OPTION:
|
|
191
|
+
return '10 kg';
|
|
192
|
+
case COMPONENT_TYPES.EMAIL:
|
|
193
|
+
return 'test@example.com';
|
|
194
|
+
case COMPONENT_TYPES.MANAGE_LIST: {
|
|
195
|
+
return `3 ${q.title}`;
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
return 'test';
|
|
199
|
+
}
|
|
200
|
+
}
|