@planning-inspectorate/dynamic-forms 3.3.0 → 3.3.1
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 +11 -0
- package/src/controller.test.js +13 -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,6 @@
|
|
|
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';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {import('./journey/journey.js').Journey} Journey
|
|
@@ -249,6 +250,16 @@ export function buildSave(saveData, redirectToTaskListOnSuccess) {
|
|
|
249
250
|
if (redirectToTaskListOnSuccess) {
|
|
250
251
|
return res.redirect(journey.taskListUrl);
|
|
251
252
|
}
|
|
253
|
+
// edit the journey.response which question.shouldDisplay uses
|
|
254
|
+
// we need to ensure the latest answer just submitted is included
|
|
255
|
+
// as question.shouldDisplay checks the response and is used to determine the next question
|
|
256
|
+
let answers = journeyResponse.answers;
|
|
257
|
+
if (question.isInManageListSection) {
|
|
258
|
+
answers = answerObjectForManageListSaving(journeyResponse, manageListQuestion, req.params);
|
|
259
|
+
}
|
|
260
|
+
for (const [k, v] of Object.entries(data?.answers || {})) {
|
|
261
|
+
answers[k] = v;
|
|
262
|
+
}
|
|
252
263
|
// move to the next question
|
|
253
264
|
return journey.redirectToNextQuestion(res, req.params, manageListQuestion);
|
|
254
265
|
} catch (err) {
|
package/src/controller.test.js
CHANGED
|
@@ -379,6 +379,7 @@ describe('dynamic-form/controller', () => {
|
|
|
379
379
|
const saveData = mock.fn();
|
|
380
380
|
await buildSave(saveData)(req, res, journeyId);
|
|
381
381
|
|
|
382
|
+
assert.strictEqual(sampleQuestionObj.getDataToSave.mock.callCount(), 1);
|
|
382
383
|
assert.strictEqual(saveData.mock.callCount(), 1);
|
|
383
384
|
assert.deepStrictEqual(saveData.mock.calls[0].arguments, [
|
|
384
385
|
{
|
|
@@ -499,6 +500,9 @@ describe('dynamic-form/controller', () => {
|
|
|
499
500
|
const sampleQuestionObjWithActions = {
|
|
500
501
|
...sampleQuestionObj
|
|
501
502
|
};
|
|
503
|
+
sampleQuestionObjWithActions.getDataToSave.mock.mockImplementationOnce(() => ({
|
|
504
|
+
answers: { [sampleQuestionObj.fieldName]: 'my-answer' }
|
|
505
|
+
}));
|
|
502
506
|
|
|
503
507
|
req.params = {
|
|
504
508
|
referenceId: mockRef,
|
|
@@ -522,6 +526,8 @@ describe('dynamic-form/controller', () => {
|
|
|
522
526
|
assert.strictEqual(saveData.mock.callCount(), 1);
|
|
523
527
|
assert.strictEqual(res.redirect.mock.callCount(), 1);
|
|
524
528
|
assert.strictEqual(res.redirect.mock.calls[0].arguments[0], expectedUrl);
|
|
529
|
+
// journey response should be edited if redirecting to next question
|
|
530
|
+
assert.strictEqual(res.locals.journeyResponse.answers?.sampleFieldName, 'my-answer');
|
|
525
531
|
});
|
|
526
532
|
|
|
527
533
|
it('should redirect to task list if configured', async () => {
|
|
@@ -531,6 +537,9 @@ describe('dynamic-form/controller', () => {
|
|
|
531
537
|
const sampleQuestionObjWithActions = {
|
|
532
538
|
...sampleQuestionObj
|
|
533
539
|
};
|
|
540
|
+
sampleQuestionObjWithActions.getDataToSave.mock.mockImplementationOnce(() => ({
|
|
541
|
+
answers: { [sampleQuestionObj.fieldName]: 'my-answer' }
|
|
542
|
+
}));
|
|
534
543
|
|
|
535
544
|
req.params = {
|
|
536
545
|
referenceId: mockRef,
|
|
@@ -555,6 +564,8 @@ describe('dynamic-form/controller', () => {
|
|
|
555
564
|
assert.strictEqual(res.redirect.mock.callCount(), 1);
|
|
556
565
|
assert.match(res.redirect.mock.calls[0].arguments[0], /\/task-list$/);
|
|
557
566
|
assert.strictEqual(mockJourney.getNextQuestionUrl.mock.callCount(), 0);
|
|
567
|
+
// journey response should not be edited if redirecting to task list
|
|
568
|
+
assert.strictEqual(Object.keys(res.locals.journeyResponse.answers).length, 0);
|
|
558
569
|
});
|
|
559
570
|
|
|
560
571
|
describe('manageListQuestions', () => {
|
|
@@ -585,7 +596,8 @@ describe('dynamic-form/controller', () => {
|
|
|
585
596
|
const journey = {
|
|
586
597
|
getSection: mock.fn(() => ({ segment: 'section-1' })),
|
|
587
598
|
getQuestionByParams: mock.fn(),
|
|
588
|
-
redirectToNextQuestion: mock.fn()
|
|
599
|
+
redirectToNextQuestion: mock.fn(),
|
|
600
|
+
response: { answers: {} }
|
|
589
601
|
};
|
|
590
602
|
const res = {
|
|
591
603
|
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
|
+
}
|