@planning-inspectorate/dynamic-forms 3.20.3 → 3.20.5

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