@planning-inspectorate/dynamic-forms 2.4.0 → 3.0.0

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/components/manage-list/README.md +97 -0
  3. package/src/components/manage-list/answer-summary-list.njk +10 -0
  4. package/src/components/manage-list/index.njk +61 -0
  5. package/src/components/manage-list/manage-list-section.js +39 -0
  6. package/src/components/manage-list/manage-list-section.test.js +52 -0
  7. package/src/components/manage-list/question.js +147 -0
  8. package/src/components/manage-list/question.test.js +139 -0
  9. package/src/components/utils/nunjucks.js +20 -0
  10. package/src/controller.js +39 -10
  11. package/src/controller.test.js +90 -24
  12. package/src/index.js +3 -2
  13. package/src/journey/journey-types.d.ts +11 -1
  14. package/src/journey/journey.js +64 -18
  15. package/src/journey/journey.test.js +80 -15
  16. package/src/questions/question-props.d.ts +2 -1
  17. package/src/questions/question.js +60 -12
  18. package/src/questions/question.test.js +26 -3
  19. package/src/questions/questions.js +3 -1
  20. package/src/section-types.d.ts +18 -0
  21. package/src/section.js +40 -11
  22. package/src/section.test.js +89 -0
  23. package/test/journey.js +5 -0
  24. package/test/mock/journey.js +17 -0
  25. package/test/questions.js +30 -1
  26. package/test/questions.test.js +104 -40
  27. package/test/snapshots/check-your-answers-not-started.html +11 -0
  28. package/test/snapshots/check-your-answers-with-answers.html +11 -0
  29. package/test/snapshots/manage-list-answer-summary.html +14 -0
  30. package/test/snapshots/manage-list-render-questions.html +97 -0
  31. package/test/snapshots/manage-list-render.html +91 -0
  32. package/test/snapshots/travel-companions.html +19 -0
  33. package/test/snapshots/travel-companions_travel-companion-email.html +21 -0
  34. package/test/snapshots/travel-companions_travel-companion-name.html +21 -0
@@ -63,6 +63,7 @@ describe('./src/dynamic-forms/question.js', () => {
63
63
  assert.strictEqual(question.validators, VALIDATORS);
64
64
  assert.strictEqual(question.html, HTML);
65
65
  assert.strictEqual(question.hint, HINT);
66
+ assert.strictEqual(question.isManageListQuestion, false);
66
67
  });
67
68
  it('should support viewData field', () => {
68
69
  const question = getTestQuestion({ viewData: { test: 'test' } });
@@ -110,6 +111,30 @@ describe('./src/dynamic-forms/question.js', () => {
110
111
  });
111
112
  });
112
113
 
114
+ describe('isInManageListSection', () => {
115
+ it('should default to false', () => {
116
+ const question = getTestQuestion();
117
+ assert.strictEqual(question.isInManageListSection, false);
118
+ });
119
+ it('should support setting isInManageListSection', () => {
120
+ const question = getTestQuestion();
121
+ assert.strictEqual(question.isInManageListSection, false);
122
+ question.isInManageListSection = true;
123
+ assert.strictEqual(question.isInManageListSection, true);
124
+ });
125
+ it('should not support setting isInManageListSection false', () => {
126
+ const question = getTestQuestion();
127
+ assert.strictEqual(question.isInManageListSection, false);
128
+ assert.throws(
129
+ () => (question.isInManageListSection = false),
130
+ (error) => {
131
+ assert.strictEqual(error.message, 'Question isInManageListSection is false by default');
132
+ return true;
133
+ }
134
+ );
135
+ });
136
+ });
137
+
113
138
  describe('prepQuestionForRendering', () => {
114
139
  it('should prepQuestionForRendering', () => {
115
140
  const question = getTestQuestion();
@@ -150,8 +175,6 @@ describe('./src/dynamic-forms/question.js', () => {
150
175
  assert.deepStrictEqual(result.answer, journey.response.answers[question.fieldName]);
151
176
  assert.deepStrictEqual(result.layoutTemplate, journey.journeyTemplate);
152
177
  assert.deepStrictEqual(result.pageCaption, section.name);
153
- assert.deepStrictEqual(result.navigation, ['', 'back']);
154
- assert.deepStrictEqual(result.backLink, 'back');
155
178
  assert.deepStrictEqual(result.showBackToListLink, question.showBackToListLink);
156
179
  assert.deepStrictEqual(result.listLink, journey.taskListUrl);
157
180
  assert.deepStrictEqual(result.journeyTitle, journey.journeyTitle);
@@ -205,7 +228,7 @@ describe('./src/dynamic-forms/question.js', () => {
205
228
  const expectedResult = { a: 1 };
206
229
  const req = { body: { errors: { error: 'we have an error' } } };
207
230
  const question = getTestQuestion();
208
- question.prepQuestionForRendering = mock.fn(() => expectedResult);
231
+ question.toViewModel = mock.fn(() => expectedResult);
209
232
 
210
233
  const result = question.checkForValidationErrors(req);
211
234
 
@@ -15,6 +15,7 @@ import EmailQuestion from '../components/email/question.js';
15
15
  import { COMPONENT_TYPES } from '../index.js';
16
16
  import TextEntryRedactQuestion from '../components/text-entry-redact/question.js';
17
17
  import DateTimeQuestion from '../components/date-time/question.js';
18
+ import ManageListQuestion from '../components/manage-list/question.js';
18
19
 
19
20
  // This looks a bit grim because so few of our
20
21
  // Questions overlap with Question correctly.
@@ -35,5 +36,6 @@ export const questionClasses = Object.freeze({
35
36
  [COMPONENT_TYPES.MULTI_FIELD_INPUT]: MultiFieldInputQuestion,
36
37
  [COMPONENT_TYPES.NUMBER]: NumberEntryQuestion,
37
38
  [COMPONENT_TYPES.UNIT_OPTION]: UnitOptionEntryQuestion,
38
- [COMPONENT_TYPES.EMAIL]: EmailQuestion
39
+ [COMPONENT_TYPES.EMAIL]: EmailQuestion,
40
+ [COMPONENT_TYPES.MANAGE_LIST]: ManageListQuestion
39
41
  });
@@ -0,0 +1,18 @@
1
+ import { JourneyResponse } from '#src/journey/journey-response.js';
2
+ import ManageListQuestion from '#src/components/manage-list/question.js';
3
+ import { Question } from '#src/questions/question.js';
4
+
5
+ export interface GetNextQuestionParams {
6
+ questionFieldName: string;
7
+ response: JourneyResponse;
8
+ // if this is part of a manage list section
9
+ manageListQuestion?: ManageListQuestion;
10
+ takeNextQuestion: boolean;
11
+ // to get previous question instead of next
12
+ reverse: boolean;
13
+ }
14
+
15
+ export interface StaticGetNextQuestionParams extends GetNextQuestionParams {
16
+ manageListQuestion: undefined;
17
+ questions: Question[];
18
+ }
package/src/section.js CHANGED
@@ -82,13 +82,20 @@ export class Section {
82
82
 
83
83
  /**
84
84
  * Fluent API method for adding questions
85
- * @param {any} question
85
+ * @param {import('#src/questions/question.js').Question} question
86
+ * @param {import('#src/components/manage-list/manage-list-section.js').ManageListSection} [manageListSection]
86
87
  * @returns {Section}
87
88
  */
88
- addQuestion(question) {
89
+ addQuestion(question, manageListSection) {
89
90
  if (!question) {
90
91
  throw new Error('question is required');
91
92
  }
93
+ if (question.isManageListQuestion) {
94
+ if (!manageListSection || !manageListSection.isManageListSection) {
95
+ throw new Error('manage list questions require a ManageListSection');
96
+ }
97
+ question.section = manageListSection;
98
+ }
92
99
  this.questions.push(question);
93
100
  this.#conditionAdded = false; // reset condition flag
94
101
  this.#applyConditions(question);
@@ -192,24 +199,46 @@ export class Section {
192
199
 
193
200
  /**
194
201
  * Get the next question in this section given a questionParam (question fieldName)
195
- * @param {Object} params
196
- * @param {string} params.questionFieldName
197
- * @param {import('#src/journey/journey-response.js').JourneyResponse} params.response
198
- * @param {boolean} [params.takeNextQuestion]
199
- * @param {boolean} [params.reverse]
202
+ * @param {import('./section-types.d.ts').GetNextQuestionParams} params
203
+ * @returns {Question|Symbol|null}
204
+ */
205
+ getNextQuestion(params) {
206
+ const { manageListQuestion } = params;
207
+ if (manageListQuestion) {
208
+ // first check if the next question is within the manage list section
209
+ const next = Section.getNextQuestion({
210
+ ...params,
211
+ questions: manageListQuestion.section.questions
212
+ });
213
+ if (next === END_OF_SECTION) {
214
+ // after the manage list section questions, go back to the manage list question
215
+ return manageListQuestion;
216
+ }
217
+ return next;
218
+ }
219
+ return Section.getNextQuestion({
220
+ ...params,
221
+ questions: this.questions
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Implementation of getNextQuestion given a list of questions
227
+ *
228
+ * @param {import('./section-types.d.ts').StaticGetNextQuestionParams} params
200
229
  * @returns {Question|Symbol|null}
201
230
  */
202
- getNextQuestion({ questionFieldName, response, takeNextQuestion = false, reverse = false }) {
203
- const numberOfQuestions = this.questions.length;
231
+ static getNextQuestion({ questions, questionFieldName, response, takeNextQuestion = false, reverse = false }) {
232
+ const numberOfQuestions = questions.length;
204
233
 
205
234
  const questionsStart = reverse ? numberOfQuestions - 1 : 0;
206
235
  for (let i = questionsStart; reverse ? i >= 0 : i < numberOfQuestions; reverse ? i-- : i++) {
207
- const question = this.questions[i];
236
+ const question = questions[i];
208
237
  if (takeNextQuestion && question.shouldDisplay(response)) {
209
238
  return question;
210
239
  }
211
240
 
212
- if (question.fieldName === questionFieldName) {
241
+ if (question.fieldName === questionFieldName || question.url === questionFieldName) {
213
242
  takeNextQuestion = true;
214
243
  }
215
244
  }
@@ -53,6 +53,42 @@ describe('./src/dynamic-forms/section.js', () => {
53
53
  assert.strictEqual(question.isRequired, mockQuestion.isRequired);
54
54
  assert.strictEqual(question.shouldDisplay(), mockQuestion.shouldDisplay());
55
55
  });
56
+
57
+ it('should support manage list questions', () => {
58
+ const section = new Section('s1', 'S');
59
+ assert.strictEqual(section.questions.length, 0);
60
+ const q = {
61
+ get isManageListQuestion() {
62
+ return true;
63
+ }
64
+ };
65
+ const manageListSection = {
66
+ get isManageListSection() {
67
+ return true;
68
+ }
69
+ };
70
+ section.addQuestion(q, manageListSection);
71
+ assert.strictEqual(section.questions.length, 1);
72
+ assert.strictEqual(q.section, manageListSection);
73
+ });
74
+
75
+ it('should throw if no manageListSection provided for a manage list question', () => {
76
+ const section = new Section('s1', 'S');
77
+ assert.strictEqual(section.questions.length, 0);
78
+ const q = {
79
+ get isManageListQuestion() {
80
+ return true;
81
+ }
82
+ };
83
+ assert.throws(
84
+ () => section.addQuestion(q),
85
+ (thrown) => {
86
+ assert.strictEqual(thrown.message, 'manage list questions require a ManageListSection');
87
+ return true;
88
+ }
89
+ );
90
+ assert.strictEqual(section.questions.length, 0);
91
+ });
56
92
  });
57
93
 
58
94
  describe('getStatus', () => {
@@ -660,5 +696,58 @@ describe('./src/dynamic-forms/section.js', () => {
660
696
  assert.strictEqual(q3.shouldDisplay.mock.callCount(), 1);
661
697
  assert.strictEqual(next, END_OF_SECTION);
662
698
  });
699
+ describe('manageListQuestions', () => {
700
+ const newSectionWithManageList = () => {
701
+ const q1 = { ...mockQuestion };
702
+ const manageListQ = {
703
+ get isManageListQuestion() {
704
+ return true;
705
+ }
706
+ };
707
+ const q3 = { ...mockQuestion2 };
708
+ const q4 = { ...mockQuestion3 };
709
+ const manageListSection = {
710
+ questions: [q3, q4],
711
+ get isManageListSection() {
712
+ return true;
713
+ }
714
+ };
715
+ const section = new Section('s1', 'S').addQuestion(q1).addQuestion(manageListQ, manageListSection);
716
+ return { section, q1, manageListQ, q3, q4 };
717
+ };
718
+ it(`should get first question within a manage list section`, () => {
719
+ const { section, manageListQ, q3 } = newSectionWithManageList();
720
+ const next = section.getNextQuestion({
721
+ questionFieldName: manageListQ.fieldName,
722
+ response: {},
723
+ manageListQuestion: manageListQ,
724
+ takeNextQuestion: true,
725
+ reverse: false
726
+ });
727
+ assert.strictEqual(next, q3);
728
+ });
729
+ it(`should get second question within a manage list section`, () => {
730
+ const { section, manageListQ, q3, q4 } = newSectionWithManageList();
731
+ const next = section.getNextQuestion({
732
+ questionFieldName: q3.fieldName,
733
+ response: {},
734
+ manageListQuestion: manageListQ,
735
+ takeNextQuestion: false,
736
+ reverse: false
737
+ });
738
+ assert.strictEqual(next, q4);
739
+ });
740
+ it(`should return to manage list question after manage list section is complete`, () => {
741
+ const { section, manageListQ, q4 } = newSectionWithManageList();
742
+ const next = section.getNextQuestion({
743
+ questionFieldName: q4.fieldName,
744
+ response: {},
745
+ manageListQuestion: manageListQ,
746
+ takeNextQuestion: false,
747
+ reverse: false
748
+ });
749
+ assert.strictEqual(next, manageListQ);
750
+ });
751
+ });
663
752
  });
664
753
  });
package/test/journey.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Section } from '../src/section.js';
2
2
  import { Journey } from '../src/journey/journey.js';
3
+ import { ManageListSection } from '#src/components/manage-list/manage-list-section.js';
3
4
 
4
5
  export const JOURNEY_ID = 'holiday-journey';
5
6
 
@@ -29,6 +30,10 @@ export function createJourney(questions, response) {
29
30
  .addQuestion(questions.hotelAddress)
30
31
  .addQuestion(questions.luggageWeight)
31
32
  .addQuestion(questions.contactEmail)
33
+ .addQuestion(
34
+ questions.travelCompanions,
35
+ new ManageListSection().addQuestion(questions.travelCompanionName).addQuestion(questions.travelCompanionEmail)
36
+ )
32
37
  ],
33
38
  taskListUrl: 'check-your-answers',
34
39
  journeyTemplate: 'views/layout-journey.njk',
@@ -0,0 +1,17 @@
1
+ import { Journey } from '#src/journey/journey.js';
2
+
3
+ export function mockJourney() {
4
+ return new Journey({
5
+ journeyId: 'mock-journey-id',
6
+ makeBaseUrl: () => '/journey',
7
+ taskListUrl: 'check-your-answers',
8
+ response: { answers: {} },
9
+ journeyTemplate: 'views/layout-journey.njk',
10
+ taskListTemplate: 'views/layout-check-your-answers.njk',
11
+ informationPageViewPath: '',
12
+ journeyTitle: 'Mock Journey',
13
+ returnToListing: false,
14
+ sections: [],
15
+ initialBackLink: '/journey'
16
+ });
17
+ }
package/test/questions.js CHANGED
@@ -174,6 +174,27 @@ export const questionProps = {
174
174
  url: 'contact-email',
175
175
  label: 'Email address',
176
176
  validators: [new EmailValidator()]
177
+ },
178
+ travelCompanions: {
179
+ type: COMPONENT_TYPES.MANAGE_LIST,
180
+ title: 'Travel Companions',
181
+ question: 'Travel Companions',
182
+ fieldName: 'travelCompanions',
183
+ url: 'travel-companions'
184
+ },
185
+ travelCompanionName: {
186
+ type: COMPONENT_TYPES.SINGLE_LINE_INPUT,
187
+ title: 'Travel Companion Name',
188
+ question: 'Travel Companion Name',
189
+ fieldName: 'travelCompanionName',
190
+ url: 'travel-companion-name'
191
+ },
192
+ travelCompanionEmail: {
193
+ type: COMPONENT_TYPES.EMAIL,
194
+ title: 'Travel Companion Email',
195
+ question: 'Travel Companion Email',
196
+ fieldName: 'travelCompanionEmail',
197
+ url: 'travel-companion-email'
177
198
  }
178
199
  };
179
200
 
@@ -194,7 +215,15 @@ export const questionsInOrder = [
194
215
  questionProps.nights,
195
216
  questionProps.hotelAddress,
196
217
  questionProps.luggageWeight,
197
- questionProps.contactEmail
218
+ questionProps.contactEmail,
219
+ questionProps.travelCompanions
220
+ ];
221
+
222
+ export const manageListQuestions = [
223
+ {
224
+ manageListQuestion: questionProps.travelCompanions,
225
+ questions: [questionProps.travelCompanionName, questionProps.travelCompanionEmail]
226
+ }
198
227
  ];
199
228
 
200
229
  export const getQuestions = () => createQuestions(questionProps, questionClasses, {});
@@ -2,7 +2,7 @@ import { describe, it } from 'node:test';
2
2
  import assert from 'assert';
3
3
  import { createApp } from './utils/app.js';
4
4
  import { TestServer } from './utils/test-server.js';
5
- import { getQuestions, questionsInOrder } from './questions.js';
5
+ import { getQuestions, manageListQuestions, questionsInOrder } from './questions.js';
6
6
  import { buildGetJourney } from '../src/middleware/build-get-journey.js';
7
7
  import { createJourney, JOURNEY_ID } from './journey.js';
8
8
  import { buildGetJourneyResponseFromSession, saveDataToSession } from '../src/lib/session-answer-store.js';
@@ -13,6 +13,7 @@ import path from 'path';
13
13
  import { COMPONENT_TYPES } from '../src/index.js';
14
14
  import { escapeForRegExp, snapshotsDir } from './utils/utils.js';
15
15
  import { BOOLEAN_OPTIONS } from '../src/components/boolean/question.js';
16
+ import { mockRandomUUID } from '#src/lib/uuid.test.js';
16
17
 
17
18
  /**
18
19
  * @param {import('node:test').TestContext} ctx
@@ -29,10 +30,15 @@ export async function createAppWithQuestions(ctx) {
29
30
  // next();
30
31
  // });
31
32
 
32
- app.get('/:section/:question', getJourneyResponse, getJourney, question);
33
+ app.get(
34
+ '/:section/:question{/:manageListAction/:manageListItemId/:manageListQuestion}',
35
+ getJourneyResponse,
36
+ getJourney,
37
+ question
38
+ );
33
39
 
34
40
  app.post(
35
- '/:section/:question',
41
+ '/:section/:question{/:manageListAction/:manageListItemId/:manageListQuestion}',
36
42
  getJourneyResponse,
37
43
  getJourney,
38
44
  validate,
@@ -83,50 +89,72 @@ describe('question pages', () => {
83
89
  }
84
90
  });
85
91
 
92
+ /**
93
+ * @param {import('node:test').TestContext} ctx
94
+ * @param {string} url
95
+ * @param {string} snapshotName
96
+ * @param {import('#src/questions/question.js')} q
97
+ * @returns {Promise<void>}
98
+ */
99
+ async function renderQuestionCheck(ctx, url, snapshotName, q) {
100
+ mockRandomUUID(ctx);
101
+ const testServer = await createAppWithQuestions(ctx);
102
+
103
+ const response = await testServer.get(url, {
104
+ redirect: 'manual'
105
+ });
106
+ assert.strictEqual(response.status, 200);
107
+ const text = await response.text();
108
+ // be sure to escape parentheses in the question text for the regex
109
+ assert.match(text, new RegExp(escapeForRegExp(q.question), 'i'));
110
+ assert.match(text, /<form action="" method="post"/i);
111
+ if (q.options) {
112
+ for (const option of q.options) {
113
+ if (q.type === COMPONENT_TYPES.SELECT) {
114
+ assert.match(
115
+ text,
116
+ new RegExp(`<option[^>]*value="${option.value}"[^>]*>\\s*${option.text}\\s*</option>`, 'i')
117
+ );
118
+ } else {
119
+ assert.match(text, new RegExp(`<input[^>]*value="${option.value}"[^>]*>`));
120
+ assert.match(text, new RegExp(`<label[^>]*>\\s*${option.text}\\s*</label>`, 'i'));
121
+ }
122
+ }
123
+ }
124
+ ctx.assert.fileSnapshot(text, path.join(snapshotsDir(), snapshotName + '.html'), {
125
+ serializers: [(v) => v]
126
+ });
127
+ }
128
+
129
+ /**
130
+ * @param {import('node:test').TestContext} ctx
131
+ * @param {string} url
132
+ * @param {import('#src/questions/question.js')} q
133
+ * @returns {Promise<string>}
134
+ */
135
+ async function postQuestionCheck(ctx, url, q) {
136
+ const testServer = await createAppWithQuestions(ctx);
137
+ const payload = mockAnswerBody(q);
138
+ const response = await testServer.post(url, payload, { redirect: 'manual' });
139
+ if (![302, 303].includes(response.status)) {
140
+ const text = await response.text();
141
+ console.log(`Response for ${q.url}:\n`, text);
142
+ }
143
+ // Should redirect (302 or 303)
144
+ assert.ok([302, 303].includes(response.status), `Expected redirect, got ${response.status}`);
145
+ // Should redirect to the next question (if there is one)
146
+ return response.headers.get('location');
147
+ }
148
+
86
149
  for (let i = 0; i < questionsInOrder.length; i++) {
87
150
  const q = questionsInOrder[i];
88
151
 
89
152
  it(`should render question: ${q.url}`, async (ctx) => {
90
- const testServer = await createAppWithQuestions(ctx);
91
-
92
- const response = await testServer.get('/questions/' + q.url, {
93
- redirect: 'manual'
94
- });
95
- assert.strictEqual(response.status, 200);
96
- const text = await response.text();
97
- // be sure to escape parentheses in the question text for the regex
98
- assert.match(text, new RegExp(escapeForRegExp(q.question), 'i'));
99
- assert.match(text, /<form action="" method="post"/i);
100
- if (q.options) {
101
- for (const option of q.options) {
102
- if (q.type === COMPONENT_TYPES.SELECT) {
103
- assert.match(
104
- text,
105
- new RegExp(`<option[^>]*value="${option.value}"[^>]*>\\s*${option.text}\\s*</option>`, 'i')
106
- );
107
- } else {
108
- assert.match(text, new RegExp(`<input[^>]*value="${option.value}"[^>]*>`));
109
- assert.match(text, new RegExp(`<label[^>]*>\\s*${option.text}\\s*</label>`, 'i'));
110
- }
111
- }
112
- }
113
- ctx.assert.fileSnapshot(text, path.join(snapshotsDir(), q.url + '.html'), {
114
- serializers: [(v) => v]
115
- });
153
+ await renderQuestionCheck(ctx, '/questions/' + q.url, q.url, q);
116
154
  });
117
155
 
118
156
  it(`should POST valid data for question: ${q.url} and redirect to next`, async (ctx) => {
119
- const testServer = await createAppWithQuestions(ctx);
120
- const payload = mockAnswerBody(q);
121
- const response = await testServer.post(`/questions/${q.url}`, payload, { redirect: 'manual' });
122
- if (![302, 303].includes(response.status)) {
123
- const text = await response.text();
124
- console.log(`Response for ${q.url}:\n`, text);
125
- }
126
- // Should redirect (302 or 303)
127
- assert.ok([302, 303].includes(response.status), `Expected redirect, got ${response.status}`);
128
- // Should redirect to the next question (if there is one)
129
- const location = response.headers.get('location');
157
+ const location = await postQuestionCheck(ctx, `/questions/${q.url}`, q);
130
158
  const nextQ = questionsInOrder[i + 1];
131
159
  if (nextQ) {
132
160
  assert.strictEqual(location, `questions/${nextQ.url}`);
@@ -135,6 +163,29 @@ describe('question pages', () => {
135
163
  }
136
164
  });
137
165
  }
166
+
167
+ describe('manage list questions', () => {
168
+ for (const { manageListQuestion, questions } of manageListQuestions) {
169
+ const qUrl = (q) => ['questions', manageListQuestion.url, 'add', 'id', q.url].join('/');
170
+ for (let i = 0; i < questions.length; i++) {
171
+ const q = questions[i];
172
+ it(`should render question: ${q.url}`, async (ctx) => {
173
+ await renderQuestionCheck(ctx, '/' + qUrl(q), manageListQuestion.url + '_' + q.url, q);
174
+ });
175
+
176
+ it(`should POST valid data for question: ${q.url} and redirect to next`, async (ctx) => {
177
+ const location = await postQuestionCheck(ctx, '/' + qUrl(q), q);
178
+ const nextQ = questions[i + 1];
179
+ if (nextQ) {
180
+ assert.strictEqual(location, qUrl(nextQ));
181
+ } else {
182
+ // at the end of the manage list section, return to the manage list question
183
+ assert.strictEqual(location, 'questions/' + manageListQuestion.url);
184
+ }
185
+ });
186
+ }
187
+ }
188
+ });
138
189
  });
139
190
 
140
191
  /**
@@ -202,6 +253,16 @@ export function mockAnswerBody(q) {
202
253
  };
203
254
  case COMPONENT_TYPES.EMAIL:
204
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
+ { travelCompanionName: 'Companion 1', travelCompanionEmail: 'compaion-1@example.com' },
261
+ { travelCompanionName: 'Companion 2', travelCompanionEmail: 'compaion-2@example.com' },
262
+ { travelCompanionName: 'Companion 3', travelCompanionEmail: 'compaion-3@example.com' }
263
+ ]
264
+ };
265
+ }
205
266
  default:
206
267
  return { [q.fieldName]: 'test' };
207
268
  }
@@ -251,6 +312,9 @@ export function mockAnswer(q) {
251
312
  return '10 kg';
252
313
  case COMPONENT_TYPES.EMAIL:
253
314
  return 'test@example.com';
315
+ case COMPONENT_TYPES.MANAGE_LIST: {
316
+ return `3 ${q.title}`;
317
+ }
254
318
  default:
255
319
  return 'test';
256
320
  }
@@ -196,6 +196,17 @@
196
196
  <a class="govuk-link" href="questions/contact-email">Answer<span class="govuk-visually-hidden"> What is your email address for booking confirmations?</span></a>
197
197
  </dd>
198
198
  </div>
199
+ <div class="govuk-summary-list__row">
200
+ <dt class="govuk-summary-list__key">
201
+ Travel Companions
202
+ </dt>
203
+ <dd class="govuk-summary-list__value">
204
+ Not started
205
+ </dd>
206
+ <dd class="govuk-summary-list__actions">
207
+ <a class="govuk-link" href="questions/travel-companions">Answer<span class="govuk-visually-hidden"> Travel Companions</span></a>
208
+ </dd>
209
+ </div>
199
210
  </dl>
200
211
 
201
212
  <form action method="post">
@@ -196,6 +196,17 @@
196
196
  <a class="govuk-link" href="questions/contact-email">Change<span class="govuk-visually-hidden"> What is your email address for booking confirmations?</span></a>
197
197
  </dd>
198
198
  </div>
199
+ <div class="govuk-summary-list__row">
200
+ <dt class="govuk-summary-list__key">
201
+ Travel Companions
202
+ </dt>
203
+ <dd class="govuk-summary-list__value">
204
+ 3 Travel Companions
205
+ </dd>
206
+ <dd class="govuk-summary-list__actions">
207
+ <a class="govuk-link" href="questions/travel-companions">Change<span class="govuk-visually-hidden"> Travel Companions</span></a>
208
+ </dd>
209
+ </div>
199
210
  </dl>
200
211
 
201
212
  <form action method="post">
@@ -0,0 +1,14 @@
1
+ <ul>
2
+ <li>
3
+ mock answer<br>
4
+ mock answer 2
5
+ </li>
6
+ <li>
7
+ mock answer<br>
8
+ mock answer 2
9
+ </li>
10
+ <li>
11
+ mock answer<br>
12
+ mock answer 2
13
+ </li>
14
+ </ul>