@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.
- package/package.json +1 -1
- package/src/components/manage-list/README.md +97 -0
- package/src/components/manage-list/answer-summary-list.njk +10 -0
- package/src/components/manage-list/index.njk +61 -0
- package/src/components/manage-list/manage-list-section.js +39 -0
- package/src/components/manage-list/manage-list-section.test.js +52 -0
- package/src/components/manage-list/question.js +147 -0
- package/src/components/manage-list/question.test.js +139 -0
- package/src/components/utils/nunjucks.js +20 -0
- package/src/controller.js +39 -10
- package/src/controller.test.js +90 -24
- package/src/index.js +3 -2
- package/src/journey/journey-types.d.ts +11 -1
- package/src/journey/journey.js +64 -18
- package/src/journey/journey.test.js +80 -15
- package/src/questions/question-props.d.ts +2 -1
- package/src/questions/question.js +60 -12
- package/src/questions/question.test.js +26 -3
- package/src/questions/questions.js +3 -1
- package/src/section-types.d.ts +18 -0
- package/src/section.js +40 -11
- package/src/section.test.js +89 -0
- package/test/journey.js +5 -0
- package/test/mock/journey.js +17 -0
- package/test/questions.js +30 -1
- package/test/questions.test.js +104 -40
- package/test/snapshots/check-your-answers-not-started.html +11 -0
- package/test/snapshots/check-your-answers-with-answers.html +11 -0
- package/test/snapshots/manage-list-answer-summary.html +14 -0
- package/test/snapshots/manage-list-render-questions.html +97 -0
- package/test/snapshots/manage-list-render.html +91 -0
- package/test/snapshots/travel-companions.html +19 -0
- package/test/snapshots/travel-companions_travel-companion-email.html +21 -0
- 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.
|
|
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 {
|
|
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 {
|
|
196
|
-
* @
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 =
|
|
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 =
|
|
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
|
}
|
package/src/section.test.js
CHANGED
|
@@ -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, {});
|
package/test/questions.test.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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">
|