@planning-inspectorate/dynamic-forms 2.3.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 (43) hide show
  1. package/package.json +2 -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 +60 -20
  11. package/src/controller.test.js +110 -43
  12. package/src/index.js +3 -2
  13. package/src/journey/journey-types.d.ts +18 -0
  14. package/src/journey/journey.js +104 -40
  15. package/src/journey/journey.test.js +94 -23
  16. package/src/lib/uuid.js +10 -0
  17. package/src/lib/uuid.test.js +24 -0
  18. package/src/middleware/dynamic-req-files-to-req-body-files.js +1 -2
  19. package/src/middleware/dynamic-req-files-to-req-body-files.test.js +2 -2
  20. package/src/questions/question-props.d.ts +2 -1
  21. package/src/questions/question.js +67 -18
  22. package/src/questions/question.test.js +26 -3
  23. package/src/questions/questions.js +3 -1
  24. package/src/section-types.d.ts +18 -0
  25. package/src/section.js +67 -2
  26. package/src/section.test.js +176 -1
  27. package/src/validator/validator.js +1 -3
  28. package/src/validator/validator.test.js +7 -7
  29. package/test/journey.js +5 -0
  30. package/test/mock/journey.js +17 -0
  31. package/test/questions.js +30 -1
  32. package/test/questions.test.js +119 -48
  33. package/test/snapshots/check-your-answers-not-started.html +11 -0
  34. package/test/snapshots/check-your-answers-with-answers.html +11 -0
  35. package/test/snapshots/manage-list-answer-summary.html +14 -0
  36. package/test/snapshots/manage-list-render-questions.html +97 -0
  37. package/test/snapshots/manage-list-render.html +91 -0
  38. package/test/snapshots/travel-companions.html +19 -0
  39. package/test/snapshots/travel-companions_travel-companion-email.html +21 -0
  40. package/test/snapshots/travel-companions_travel-companion-name.html +21 -0
  41. package/test/utils/test-server.js +31 -9
  42. package/test/utils/utils.js +2 -1
  43. package/tsconfig.json +23 -0
@@ -1,6 +1,6 @@
1
1
  import { describe, it, mock } from 'node:test';
2
2
  import assert from 'node:assert';
3
- import { Section, SECTION_STATUS } from './section.js';
3
+ import { END_OF_SECTION, Section, SECTION_STATUS } from './section.js';
4
4
  import RequiredValidator from './validator/required-validator.js';
5
5
 
6
6
  const mockQuestion = {
@@ -17,6 +17,13 @@ const mockQuestion2 = {
17
17
  isAnswered: () => false
18
18
  };
19
19
 
20
+ const mockQuestion3 = {
21
+ fieldName: 'mockQuestion3',
22
+ isRequired: () => true,
23
+ shouldDisplay: () => true,
24
+ isAnswered: () => false
25
+ };
26
+
20
27
  describe('./src/dynamic-forms/section.js', () => {
21
28
  describe('constructor', () => {
22
29
  it('should create', () => {
@@ -46,6 +53,42 @@ describe('./src/dynamic-forms/section.js', () => {
46
53
  assert.strictEqual(question.isRequired, mockQuestion.isRequired);
47
54
  assert.strictEqual(question.shouldDisplay(), mockQuestion.shouldDisplay());
48
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
+ });
49
92
  });
50
93
 
51
94
  describe('getStatus', () => {
@@ -575,4 +618,136 @@ describe('./src/dynamic-forms/section.js', () => {
575
618
  assert.throws(() => section.withRequiredCondition(false));
576
619
  });
577
620
  });
621
+
622
+ describe('getNextQuestion', () => {
623
+ const newSection = () => {
624
+ const q1 = { ...mockQuestion };
625
+ const q2 = { ...mockQuestion2 };
626
+ const q3 = { ...mockQuestion3 };
627
+ const section = new Section('s1', 'S').addQuestion(q1).addQuestion(q2).addQuestion(q3);
628
+ return { section, q1, q2, q3 };
629
+ };
630
+
631
+ it('should take the first question if takeNextQuestion is set', () => {
632
+ const { section, q1 } = newSection();
633
+ const next = section.getNextQuestion({
634
+ questionFieldName: '',
635
+ response: {},
636
+ takeNextQuestion: true,
637
+ reverse: false
638
+ });
639
+ assert.strictEqual(next, q1);
640
+ });
641
+ it('should take second question if first is matched', () => {
642
+ const { section, q1, q2 } = newSection();
643
+ const next = section.getNextQuestion({
644
+ questionFieldName: q1.fieldName,
645
+ response: {},
646
+ takeNextQuestion: false,
647
+ reverse: false
648
+ });
649
+ assert.strictEqual(next, q2);
650
+ });
651
+ it('should return END_OF_SECTION if last question matched', () => {
652
+ const { section, q3 } = newSection();
653
+ const next = section.getNextQuestion({
654
+ questionFieldName: q3.fieldName,
655
+ response: {},
656
+ takeNextQuestion: false,
657
+ reverse: false
658
+ });
659
+ assert.strictEqual(next, END_OF_SECTION);
660
+ });
661
+ it('should return null if no questions match', () => {
662
+ const { section } = newSection();
663
+ const next = section.getNextQuestion({
664
+ questionFieldName: 'random-field-name',
665
+ response: {},
666
+ takeNextQuestion: false,
667
+ reverse: false
668
+ });
669
+ assert.strictEqual(next, null);
670
+ });
671
+ it('should return question only if it "shouldDisplay"', () => {
672
+ const { section, q1, q2, q3 } = newSection();
673
+ // skip q2
674
+ q2.shouldDisplay = mock.fn(() => false);
675
+ const next = section.getNextQuestion({
676
+ questionFieldName: q1.fieldName,
677
+ response: {},
678
+ takeNextQuestion: false,
679
+ reverse: false
680
+ });
681
+ assert.strictEqual(q2.shouldDisplay.mock.callCount(), 1);
682
+ assert.strictEqual(next, q3);
683
+ });
684
+ it(`should return END_OF_SECTION if remaining questions shouldn't display`, () => {
685
+ const { section, q1, q2, q3 } = newSection();
686
+ // skip q2 & q3
687
+ q2.shouldDisplay = mock.fn(() => false);
688
+ q3.shouldDisplay = mock.fn(() => false);
689
+ const next = section.getNextQuestion({
690
+ questionFieldName: q1.fieldName,
691
+ response: {},
692
+ takeNextQuestion: false,
693
+ reverse: false
694
+ });
695
+ assert.strictEqual(q2.shouldDisplay.mock.callCount(), 1);
696
+ assert.strictEqual(q3.shouldDisplay.mock.callCount(), 1);
697
+ assert.strictEqual(next, END_OF_SECTION);
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
+ });
752
+ });
578
753
  });
@@ -2,11 +2,9 @@ import { validationResult } from 'express-validator';
2
2
 
3
3
  /** @type {import('express').Handler} */
4
4
  const validate = async (req, res, next) => {
5
- const { section, question } = req.params;
6
-
7
5
  const { journey, journeyResponse } = res.locals;
8
6
 
9
- const questionObj = journey.getQuestionBySectionAndName(section, question);
7
+ const questionObj = journey.getQuestionByParams(req.params);
10
8
  if (!questionObj) {
11
9
  throw new Error('unknown question type');
12
10
  }
@@ -12,7 +12,7 @@ describe('./src/dynamic-forms/validator/validator.js', () => {
12
12
  locals: {
13
13
  journeyResponse: {},
14
14
  journey: {
15
- getQuestionBySectionAndName: function () {
15
+ getQuestionByParams: function () {
16
16
  return null;
17
17
  }
18
18
  }
@@ -50,7 +50,7 @@ describe('./src/dynamic-forms/validator/validator.js', () => {
50
50
  };
51
51
 
52
52
  mockRes.locals.journey = {
53
- getQuestionBySectionAndName: function () {
53
+ getQuestionByParams: function () {
54
54
  return {
55
55
  validators: [new RequiredValidator()],
56
56
  fieldName: 'field1'
@@ -78,7 +78,7 @@ describe('./src/dynamic-forms/validator/validator.js', () => {
78
78
  };
79
79
 
80
80
  mockRes.locals.journey = {
81
- getQuestionBySectionAndName: function () {
81
+ getQuestionByParams: function () {
82
82
  return {
83
83
  validators: [new RequiredValidator()],
84
84
  fieldName: 'field1'
@@ -106,7 +106,7 @@ describe('./src/dynamic-forms/validator/validator.js', () => {
106
106
  };
107
107
 
108
108
  mockRes.locals.journey = {
109
- getQuestionBySectionAndName: function () {
109
+ getQuestionByParams: function () {
110
110
  return {
111
111
  validators: [new RequiredValidator()],
112
112
  fieldName: 'field1'
@@ -133,7 +133,7 @@ describe('./src/dynamic-forms/validator/validator.js', () => {
133
133
  };
134
134
 
135
135
  mockRes.locals.journey = {
136
- getQuestionBySectionAndName: function () {
136
+ getQuestionByParams: function () {
137
137
  return {
138
138
  validators: [new RequiredValidator(), new ValidOptionValidator()],
139
139
  fieldName: 'field1',
@@ -174,7 +174,7 @@ describe('./src/dynamic-forms/validator/validator.js', () => {
174
174
  };
175
175
 
176
176
  mockRes.locals.journey = {
177
- getQuestionBySectionAndName: function () {
177
+ getQuestionByParams: function () {
178
178
  return {
179
179
  validators: [new RequiredValidator(), new ValidOptionValidator()],
180
180
  fieldName: 'field1',
@@ -217,7 +217,7 @@ describe('./src/dynamic-forms/validator/validator.js', () => {
217
217
  };
218
218
 
219
219
  mockRes.locals.journey = {
220
- getQuestionBySectionAndName: function () {
220
+ getQuestionByParams: function () {
221
221
  return {
222
222
  validators: [new AddressValidator()],
223
223
  fieldName: 'field1'
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,
@@ -42,18 +48,25 @@ export async function createAppWithQuestions(ctx) {
42
48
 
43
49
  app.get('/check-your-answers', getJourneyResponse, getJourney, (req, res) => list(req, res, '', {}));
44
50
 
45
- app.use((error, req, res, next) => {
46
- if (error) {
47
- console.error('Internal Server Error:', error);
48
- }
49
- next();
50
- });
51
-
52
51
  app.use((req, res) => {
53
52
  res.status(404);
54
53
  });
55
54
 
56
- const server = new TestServer(app, { rememberCookies: true });
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 });
57
70
  await server.start();
58
71
  ctx.after(async () => await server.stop());
59
72
  return server;
@@ -76,50 +89,72 @@ describe('question pages', () => {
76
89
  }
77
90
  });
78
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
+
79
149
  for (let i = 0; i < questionsInOrder.length; i++) {
80
150
  const q = questionsInOrder[i];
81
151
 
82
152
  it(`should render question: ${q.url}`, async (ctx) => {
83
- const testServer = await createAppWithQuestions(ctx);
84
-
85
- const response = await testServer.get('/questions/' + q.url, {
86
- redirect: 'manual'
87
- });
88
- assert.strictEqual(response.status, 200);
89
- const text = await response.text();
90
- // be sure to escape parentheses in the question text for the regex
91
- assert.match(text, new RegExp(escapeForRegExp(q.question), 'i'));
92
- assert.match(text, /<form action="" method="post"/i);
93
- if (q.options) {
94
- for (const option of q.options) {
95
- if (q.type === COMPONENT_TYPES.SELECT) {
96
- assert.match(
97
- text,
98
- new RegExp(`<option[^>]*value="${option.value}"[^>]*>\\s*${option.text}\\s*</option>`, 'i')
99
- );
100
- } else {
101
- assert.match(text, new RegExp(`<input[^>]*value="${option.value}"[^>]*>`));
102
- assert.match(text, new RegExp(`<label[^>]*>\\s*${option.text}\\s*</label>`, 'i'));
103
- }
104
- }
105
- }
106
- ctx.assert.fileSnapshot(text, path.join(snapshotsDir(), q.url + '.html'), {
107
- serializers: [(v) => v]
108
- });
153
+ await renderQuestionCheck(ctx, '/questions/' + q.url, q.url, q);
109
154
  });
110
155
 
111
156
  it(`should POST valid data for question: ${q.url} and redirect to next`, async (ctx) => {
112
- const testServer = await createAppWithQuestions(ctx);
113
- const payload = mockAnswerBody(q);
114
- const response = await testServer.post(`/questions/${q.url}`, payload, { redirect: 'manual' });
115
- if (![302, 303].includes(response.status)) {
116
- const text = await response.text();
117
- console.log(`Response for ${q.url}:\n`, text);
118
- }
119
- // Should redirect (302 or 303)
120
- assert.ok([302, 303].includes(response.status), `Expected redirect, got ${response.status}`);
121
- // Should redirect to the next question (if there is one)
122
- const location = response.headers.get('location');
157
+ const location = await postQuestionCheck(ctx, `/questions/${q.url}`, q);
123
158
  const nextQ = questionsInOrder[i + 1];
124
159
  if (nextQ) {
125
160
  assert.strictEqual(location, `questions/${nextQ.url}`);
@@ -128,6 +163,29 @@ describe('question pages', () => {
128
163
  }
129
164
  });
130
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
+ });
131
189
  });
132
190
 
133
191
  /**
@@ -195,6 +253,16 @@ export function mockAnswerBody(q) {
195
253
  };
196
254
  case COMPONENT_TYPES.EMAIL:
197
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
+ }
198
266
  default:
199
267
  return { [q.fieldName]: 'test' };
200
268
  }
@@ -244,6 +312,9 @@ export function mockAnswer(q) {
244
312
  return '10 kg';
245
313
  case COMPONENT_TYPES.EMAIL:
246
314
  return 'test@example.com';
315
+ case COMPONENT_TYPES.MANAGE_LIST: {
316
+ return `3 ${q.title}`;
317
+ }
247
318
  default:
248
319
  return 'test';
249
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>