@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/dynamic-forms",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "This package is for building GDS forms using a configuration-based approach.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -72,12 +72,11 @@ export default class AddressQuestion extends Question {
72
72
  }
73
73
 
74
74
  /**
75
- * returns the data to send to the DB
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
- * returns the data to send to the DB
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
- // set answer on response
108
- let responseToSave = { answers: {} };
106
+ const answers = {};
109
107
  const fieldValue = req.body[this.fieldName]?.trim();
110
108
 
111
- responseToSave.answers[this.fieldName] = fieldValue === BOOLEAN_OPTIONS.YES;
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
- responseToSave.answers[propName] = req.body[propName]?.trim();
116
- journeyResponse.answers[propName] = req.body[propName]?.trim();
113
+ answers[propName] = req.body[propName]?.trim();
117
114
  }
118
115
  }
119
116
 
120
- journeyResponse.answers[this.fieldName] = fieldValue;
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
- * returns the data to send to the DB
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
- // set answer on response
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
- const dateToSave = parseDateInput({ day: dayInput, month: monthInput, year: yearInput });
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 responseToSave;
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
- * returns the data to send to the DB
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
- // set answer on response
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
- responseToSave.answers[this.fieldName] = { start: startDate, end: endDate };
74
-
75
- journeyResponse.answers[this.fieldName] = responseToSave.answers[this.fieldName];
71
+ answers[this.fieldName] = { start: startDate, end: endDate };
76
72
 
77
- return responseToSave;
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
- * returns the data to send to the DB
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
- let responseToSave = { answers: {} };
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
- responseToSave.answers[this.fieldName] = parseDateInput({
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
- journeyResponse.answers[this.fieldName] = responseToSave.answers[this.fieldName];
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 journeyResponse = { answers: {} };
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 journeyResponse = { answers: {} };
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 journeyResponse = { answers: {} };
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
- let responseToSave = { answers: {} };
96
- const data = journeyResponse.answers[this.fieldName];
97
- responseToSave.answers[this.fieldName] = data || [];
98
- return responseToSave;
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 '#src/lib/uuid.test.js';
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
- * returns the data to send to the DB
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
- responseToSave.answers[inputField.fieldName] = value;
95
- journeyResponse.answers[inputField.fieldName] = responseToSave.answers[inputField.fieldName];
90
+ answers[inputField.fieldName] = value;
96
91
  }
97
92
 
98
- return responseToSave;
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
- value: nl2br(escape(formattedAnswer)),
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/single-line-input/question.js', () => {
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 &amp; 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 responseToSave = { answers: {} };
64
- responseToSave.answers[fieldName] = req.body[this.fieldName];
65
- journeyResponse.answers[fieldName] = responseToSave.answers[fieldName];
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
- * returns the data to send to the DB
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
- responseToSave.answers[this.fieldName] = fieldValues.join(this.optionJoinString);
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
- const value = req.body[option.conditional.fieldName] * conversionFactor;
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 responseToSave;
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) {
@@ -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: {
@@ -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
- * returns the data to send to the DB
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
- responseToSave.answers[this.fieldName] = fieldValues.join(this.optionJoinString);
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
- const value = optionIsSelectedOption ? req.body[key]?.trim() : null;
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 responseToSave;
153
+ return { answers };
161
154
  }
162
155
  }
@@ -338,38 +338,24 @@ export class Question {
338
338
  }
339
339
 
340
340
  /**
341
- * returns the data to send to the DB
342
- * side effect: modifies journeyResponse with the new answers
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, modified with the new answers
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
- responseToSave.answers[this.fieldName] = req.body[this.fieldName];
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
- responseToSave.answers[propName] = req.body[propName];
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
- journeyResponse.answers[this.fieldName] = responseToSave.answers[this.fieldName];
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 and modify journeyResponse', async () => {
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, journeyResponse);
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, journeyResponse);
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 './questions.test.js';
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
+ }
@@ -1,76 +1,11 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'assert';
3
- import { createApp } from './utils/app.js';
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 { BOOLEAN_OPTIONS } from '../src/components/boolean/question.js';
16
- import { mockRandomUUID } from '#src/lib/uuid.test.js';
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
+ }