@planning-inspectorate/dynamic-forms 1.13.0 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +2 -1
- package/README.md +9 -1
- package/package.json +8 -3
- package/src/components/address/question.js +1 -1
- package/src/components/address/question.test.js +3 -3
- package/src/components/multi-field-input/index.njk +2 -2
- package/src/components/multi-field-input/question.js +5 -1
- package/src/components/radio/index.njk +2 -2
- package/src/components/select/index.njk +11 -1
- package/src/components/text-entry-redact/question.test.js +9 -9
- package/src/controller.test.js +2 -1
- package/src/index.js +8 -2
- package/src/lib/session-answer-store.test.js +1 -1
- package/src/questions/question-props.d.ts +5 -24
- package/src/questions/question.test.js +1 -1
- package/src/section.js +3 -0
- package/src/validator/validator.js +4 -1
- package/test/check-your-answers.test.js +69 -0
- package/test/journey.js +41 -0
- package/test/journey.test.js +21 -0
- package/test/questions.js +189 -0
- package/test/questions.test.js +246 -0
- package/test/snapshots/add-insurance.html +44 -0
- package/test/snapshots/check-your-answers-not-started.html +201 -0
- package/test/snapshots/check-your-answers-with-answers.html +201 -0
- package/test/snapshots/companions.html +51 -0
- package/test/snapshots/departure-date.html +57 -0
- package/test/snapshots/holiday-activities.html +49 -0
- package/test/snapshots/holiday-arrival.html +101 -0
- package/test/snapshots/holiday-description.html +33 -0
- package/test/snapshots/holiday-destination.html +56 -0
- package/test/snapshots/holiday-period.html +102 -0
- package/test/snapshots/holiday-snack.html +33 -0
- package/test/snapshots/hotel-address.html +65 -0
- package/test/snapshots/luggage-weight-units.html +75 -0
- package/test/snapshots/nights.html +28 -0
- package/test/snapshots/secret-wish.html +173 -0
- package/test/snapshots/travel-class.html +52 -0
- package/test/snapshots/travel-insurance-type.html +56 -0
- package/test/utils/app.js +41 -0
- package/test/utils/nunjucks.js +20 -0
- package/test/utils/test-server.js +167 -0
- package/test/utils/test-server.test.js +75 -0
- package/{src/lib/test-utils.js → test/utils/utils.js} +15 -16
- package/test/views/layout-check-your-answers.njk +1 -0
- /package/{src/lib/test-layout.njk → test/views/layout-question.njk} +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { COMPONENT_TYPES } from '../src/index.js';
|
|
2
|
+
import { createQuestions } from '../src/questions/create-questions.js';
|
|
3
|
+
import { questionClasses } from '../src/questions/questions.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('../src/questions/question-props.js').QuestionProps} Props
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @type {Record<string, Props>} */
|
|
10
|
+
export const questionProps = {
|
|
11
|
+
holidayActivities: {
|
|
12
|
+
type: COMPONENT_TYPES.CHECKBOX,
|
|
13
|
+
title: 'Holiday Activities',
|
|
14
|
+
question: 'Which activities do you want to do on holiday?',
|
|
15
|
+
fieldName: 'holidayActivities',
|
|
16
|
+
url: 'holiday-activities',
|
|
17
|
+
label: 'Select all that apply',
|
|
18
|
+
options: [
|
|
19
|
+
{ value: 'swimming', text: 'Swimming' },
|
|
20
|
+
{ value: 'hiking', text: 'Hiking' },
|
|
21
|
+
{ value: 'sightseeing', text: 'Sightseeing' }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
addInsurance: {
|
|
25
|
+
type: COMPONENT_TYPES.BOOLEAN,
|
|
26
|
+
title: 'Travel Insurance',
|
|
27
|
+
question: 'Do you want to add travel insurance to your holiday?',
|
|
28
|
+
fieldName: 'addInsurance',
|
|
29
|
+
url: 'add-insurance',
|
|
30
|
+
label: 'Add insurance?'
|
|
31
|
+
},
|
|
32
|
+
travelInsuranceType: {
|
|
33
|
+
type: COMPONENT_TYPES.RADIO,
|
|
34
|
+
title: 'Travel Insurance Type',
|
|
35
|
+
question: 'Which type of travel insurance would you like?',
|
|
36
|
+
fieldName: 'travelInsuranceType',
|
|
37
|
+
url: 'travel-insurance-type',
|
|
38
|
+
label: 'Select an insurance type',
|
|
39
|
+
options: [
|
|
40
|
+
{ value: 'basic', text: 'Basic' },
|
|
41
|
+
{ value: 'comprehensive', text: 'Comprehensive' },
|
|
42
|
+
{ value: 'premium', text: 'Premium' }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
holidayDestination: {
|
|
46
|
+
type: COMPONENT_TYPES.RADIO,
|
|
47
|
+
title: 'Holiday Destination',
|
|
48
|
+
question: 'Where would you like to go on holiday?',
|
|
49
|
+
fieldName: 'holidayDestination',
|
|
50
|
+
url: 'holiday-destination',
|
|
51
|
+
label: 'Choose a destination',
|
|
52
|
+
options: [
|
|
53
|
+
{ value: 'beach', text: 'Beach' },
|
|
54
|
+
{ value: 'mountains', text: 'Mountains' },
|
|
55
|
+
{ value: 'city', text: 'City' }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
departureDate: {
|
|
59
|
+
type: COMPONENT_TYPES.DATE,
|
|
60
|
+
title: 'Departure Date',
|
|
61
|
+
question: 'When do you want to start your holiday?',
|
|
62
|
+
fieldName: 'departureDate',
|
|
63
|
+
url: 'departure-date',
|
|
64
|
+
label: 'Select a date'
|
|
65
|
+
},
|
|
66
|
+
holidayPeriod: {
|
|
67
|
+
type: COMPONENT_TYPES.DATE_PERIOD,
|
|
68
|
+
title: 'Holiday Period',
|
|
69
|
+
question: 'What is the period of your holiday?',
|
|
70
|
+
fieldName: 'holidayPeriod',
|
|
71
|
+
url: 'holiday-period',
|
|
72
|
+
label: 'Select start and end dates'
|
|
73
|
+
},
|
|
74
|
+
holidayDescription: {
|
|
75
|
+
type: COMPONENT_TYPES.TEXT_ENTRY,
|
|
76
|
+
title: 'Holiday Description',
|
|
77
|
+
question: 'Describe your ideal holiday.',
|
|
78
|
+
fieldName: 'holidayDescription',
|
|
79
|
+
url: 'holiday-description',
|
|
80
|
+
label: 'Description'
|
|
81
|
+
},
|
|
82
|
+
secretWish: {
|
|
83
|
+
type: COMPONENT_TYPES.TEXT_ENTRY_REDACT,
|
|
84
|
+
title: 'Secret Holiday Wish',
|
|
85
|
+
question: 'Share a secret wish for your holiday (will be redacted).',
|
|
86
|
+
fieldName: 'secretWish',
|
|
87
|
+
url: 'secret-wish',
|
|
88
|
+
label: 'Secret wish'
|
|
89
|
+
},
|
|
90
|
+
travelClass: {
|
|
91
|
+
type: COMPONENT_TYPES.SELECT,
|
|
92
|
+
title: 'Travel Class',
|
|
93
|
+
question: 'Which class do you want to travel in?',
|
|
94
|
+
fieldName: 'travelClass',
|
|
95
|
+
url: 'travel-class',
|
|
96
|
+
label: 'Select a class',
|
|
97
|
+
options: [
|
|
98
|
+
{ value: 'economy', text: 'Economy' },
|
|
99
|
+
{ value: 'business', text: 'Business' },
|
|
100
|
+
{ value: 'first', text: 'First Class' }
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
holidayArrival: {
|
|
104
|
+
type: COMPONENT_TYPES.DATE_TIME,
|
|
105
|
+
title: 'Holiday Arrival',
|
|
106
|
+
question: 'When would you like to arrive at your destination?',
|
|
107
|
+
fieldName: 'holidayArrival',
|
|
108
|
+
url: 'holiday-arrival',
|
|
109
|
+
label: 'Enter your arrival time'
|
|
110
|
+
},
|
|
111
|
+
holidaySnack: {
|
|
112
|
+
type: COMPONENT_TYPES.SINGLE_LINE_INPUT,
|
|
113
|
+
title: 'Favourite Holiday Snack',
|
|
114
|
+
question: 'What is your favourite snack to eat on holiday?',
|
|
115
|
+
fieldName: 'holidaySnack',
|
|
116
|
+
url: 'holiday-snack',
|
|
117
|
+
label: 'Snack'
|
|
118
|
+
},
|
|
119
|
+
companions: {
|
|
120
|
+
type: COMPONENT_TYPES.MULTI_FIELD_INPUT,
|
|
121
|
+
title: 'Travel Companions',
|
|
122
|
+
question: 'Enter the names and ages of your travel companions.',
|
|
123
|
+
fieldName: 'companions',
|
|
124
|
+
url: 'companions',
|
|
125
|
+
label: 'Companions',
|
|
126
|
+
inputFields: [
|
|
127
|
+
{ fieldName: 'name', label: 'Name', type: COMPONENT_TYPES.TEXT_ENTRY },
|
|
128
|
+
{ fieldName: 'age', label: 'Age', type: COMPONENT_TYPES.NUMBER }
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
nights: {
|
|
132
|
+
type: COMPONENT_TYPES.NUMBER,
|
|
133
|
+
title: 'Number of Nights',
|
|
134
|
+
question: 'How many nights will you stay on holiday?',
|
|
135
|
+
fieldName: 'nights',
|
|
136
|
+
url: 'nights',
|
|
137
|
+
label: 'Nights'
|
|
138
|
+
},
|
|
139
|
+
hotelAddress: {
|
|
140
|
+
type: COMPONENT_TYPES.ADDRESS,
|
|
141
|
+
title: 'Hotel Address',
|
|
142
|
+
question: 'What is the address of your hotel?',
|
|
143
|
+
fieldName: 'hotelAddress',
|
|
144
|
+
url: 'hotel-address',
|
|
145
|
+
label: 'Hotel address'
|
|
146
|
+
},
|
|
147
|
+
luggageWeight: {
|
|
148
|
+
type: COMPONENT_TYPES.UNIT_OPTION,
|
|
149
|
+
title: 'Luggage Weight',
|
|
150
|
+
question: 'Select your luggage weight and unit.',
|
|
151
|
+
fieldName: 'luggageWeightUnits',
|
|
152
|
+
conditionalFieldName: 'luggageWeightValue',
|
|
153
|
+
url: 'luggage-weight-units',
|
|
154
|
+
label: 'Luggage weight',
|
|
155
|
+
options: [
|
|
156
|
+
{
|
|
157
|
+
text: 'Kilograms',
|
|
158
|
+
value: 'kg',
|
|
159
|
+
conditional: { label: 'Weight in kilograms', fieldName: 'luggageWeightValue_kg', suffix: 'kg' }
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
text: 'Pounds',
|
|
163
|
+
value: 'lbs',
|
|
164
|
+
conditional: { label: 'Weight in pounds', fieldName: 'luggageWeightValue_lbs', suffix: 'lbs' }
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// questions in order for the journey - used to check the journey redirects to the next question correctly
|
|
171
|
+
export const questionsInOrder = [
|
|
172
|
+
questionProps.holidayActivities,
|
|
173
|
+
questionProps.addInsurance,
|
|
174
|
+
questionProps.travelInsuranceType,
|
|
175
|
+
questionProps.holidayDestination,
|
|
176
|
+
questionProps.departureDate,
|
|
177
|
+
questionProps.holidayPeriod,
|
|
178
|
+
questionProps.holidayDescription,
|
|
179
|
+
questionProps.secretWish,
|
|
180
|
+
questionProps.travelClass,
|
|
181
|
+
questionProps.holidayArrival,
|
|
182
|
+
questionProps.holidaySnack,
|
|
183
|
+
questionProps.companions,
|
|
184
|
+
questionProps.nights,
|
|
185
|
+
questionProps.hotelAddress,
|
|
186
|
+
questionProps.luggageWeight
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
export const getQuestions = () => createQuestions(questionProps, questionClasses, {});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
import { createApp } from './utils/app.js';
|
|
4
|
+
import { TestServer } from './utils/test-server.js';
|
|
5
|
+
import { getQuestions, questionsInOrder } from './questions.js';
|
|
6
|
+
import { buildGetJourney } from '../src/middleware/build-get-journey.js';
|
|
7
|
+
import { createJourney, JOURNEY_ID } from './journey.js';
|
|
8
|
+
import { buildGetJourneyResponseFromSession, saveDataToSession } from '../src/lib/session-answer-store.js';
|
|
9
|
+
import validate from '../src/validator/validator.js';
|
|
10
|
+
import { validationErrorHandler } from '../src/validator/validation-error-handler.js';
|
|
11
|
+
import { buildSave, list, question } from '../src/controller.js';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { COMPONENT_TYPES } from '../src/index.js';
|
|
14
|
+
import { escapeForRegExp, snapshotsDir } from './utils/utils.js';
|
|
15
|
+
import { BOOLEAN_OPTIONS } from '../src/components/boolean/question.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {import('node:test').TestContext} ctx
|
|
19
|
+
* @returns {Promise<TestServer>}
|
|
20
|
+
*/
|
|
21
|
+
export async function createAppWithQuestions(ctx) {
|
|
22
|
+
const app = createApp();
|
|
23
|
+
const questions = getQuestions();
|
|
24
|
+
const getJourney = buildGetJourney((req, journeyResponse) => createJourney(questions, journeyResponse));
|
|
25
|
+
const getJourneyResponse = buildGetJourneyResponseFromSession(JOURNEY_ID);
|
|
26
|
+
|
|
27
|
+
// app.use((req, res, next) => {
|
|
28
|
+
// console.log(req.method, req.url, req.session);
|
|
29
|
+
// next();
|
|
30
|
+
// });
|
|
31
|
+
|
|
32
|
+
app.get('/:section/:question', getJourneyResponse, getJourney, question);
|
|
33
|
+
|
|
34
|
+
app.post(
|
|
35
|
+
'/:section/:question',
|
|
36
|
+
getJourneyResponse,
|
|
37
|
+
getJourney,
|
|
38
|
+
validate,
|
|
39
|
+
validationErrorHandler,
|
|
40
|
+
buildSave(saveDataToSession)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
app.get('/check-your-answers', getJourneyResponse, getJourney, (req, res) => list(req, res, '', {}));
|
|
44
|
+
|
|
45
|
+
app.use((error, req, res, next) => {
|
|
46
|
+
if (error) {
|
|
47
|
+
console.error('Internal Server Error:', error);
|
|
48
|
+
}
|
|
49
|
+
next();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
app.use((req, res) => {
|
|
53
|
+
res.status(404);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const server = new TestServer(app, { rememberCookies: true });
|
|
57
|
+
await server.start();
|
|
58
|
+
ctx.after(async () => await server.stop());
|
|
59
|
+
return server;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('question pages', () => {
|
|
63
|
+
it('should have a test for each question type', () => {
|
|
64
|
+
const questionsByType = new Map();
|
|
65
|
+
for (const q of questionsInOrder) {
|
|
66
|
+
if (!questionsByType.has(q.type)) {
|
|
67
|
+
questionsByType.set(q.type, []);
|
|
68
|
+
}
|
|
69
|
+
questionsByType.get(q.type).push(q);
|
|
70
|
+
}
|
|
71
|
+
for (const [k, v] of Object.entries(COMPONENT_TYPES)) {
|
|
72
|
+
const questions = questionsByType.get(v);
|
|
73
|
+
if (!questions || questions.length === 0) {
|
|
74
|
+
throw new Error(`no test questions for ${k}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < questionsInOrder.length; i++) {
|
|
80
|
+
const q = questionsInOrder[i];
|
|
81
|
+
|
|
82
|
+
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
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
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');
|
|
123
|
+
const nextQ = questionsInOrder[i + 1];
|
|
124
|
+
if (nextQ) {
|
|
125
|
+
assert.strictEqual(location, `questions/${nextQ.url}`);
|
|
126
|
+
} else {
|
|
127
|
+
assert.strictEqual(location, 'check-your-answers');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build a valid payload for a question.
|
|
135
|
+
* @param {import('../src/questions/question-props.js').QuestionProps} q
|
|
136
|
+
* @returns {Object}
|
|
137
|
+
*/
|
|
138
|
+
export function mockAnswerBody(q) {
|
|
139
|
+
switch (q.type) {
|
|
140
|
+
case COMPONENT_TYPES.BOOLEAN:
|
|
141
|
+
return { [q.fieldName]: BOOLEAN_OPTIONS.YES };
|
|
142
|
+
case COMPONENT_TYPES.CHECKBOX:
|
|
143
|
+
return { [q.fieldName]: [q.options[0].value] };
|
|
144
|
+
case COMPONENT_TYPES.RADIO:
|
|
145
|
+
case COMPONENT_TYPES.SELECT:
|
|
146
|
+
return { [q.fieldName]: q.options[0].value };
|
|
147
|
+
case COMPONENT_TYPES.NUMBER:
|
|
148
|
+
return { [q.fieldName]: 1 };
|
|
149
|
+
case COMPONENT_TYPES.DATE:
|
|
150
|
+
return {
|
|
151
|
+
[q.fieldName + '_day']: 1,
|
|
152
|
+
[q.fieldName + '_month']: 1,
|
|
153
|
+
[q.fieldName + '_year']: 2025
|
|
154
|
+
};
|
|
155
|
+
case COMPONENT_TYPES.DATE_PERIOD:
|
|
156
|
+
return {
|
|
157
|
+
[q.fieldName + '_start_day']: 1,
|
|
158
|
+
[q.fieldName + '_start_month']: 1,
|
|
159
|
+
[q.fieldName + '_start_year']: 2025,
|
|
160
|
+
[q.fieldName + '_end_day']: 2,
|
|
161
|
+
[q.fieldName + '_end_month']: 1,
|
|
162
|
+
[q.fieldName + '_end_year']: 2025
|
|
163
|
+
};
|
|
164
|
+
case COMPONENT_TYPES.DATE_TIME:
|
|
165
|
+
return {
|
|
166
|
+
[q.fieldName + '_day']: 1,
|
|
167
|
+
[q.fieldName + '_month']: 1,
|
|
168
|
+
[q.fieldName + '_year']: 2025,
|
|
169
|
+
[q.fieldName + '_hour']: 10,
|
|
170
|
+
[q.fieldName + '_minutes']: 45,
|
|
171
|
+
[q.fieldName + '_period']: 'am'
|
|
172
|
+
};
|
|
173
|
+
case COMPONENT_TYPES.TEXT_ENTRY:
|
|
174
|
+
case COMPONENT_TYPES.SINGLE_LINE_INPUT:
|
|
175
|
+
return { [q.fieldName]: 'test' };
|
|
176
|
+
case COMPONENT_TYPES.TEXT_ENTRY_REDACT:
|
|
177
|
+
return { [q.fieldName]: 'secret' };
|
|
178
|
+
case COMPONENT_TYPES.MULTI_FIELD_INPUT: {
|
|
179
|
+
const res = {};
|
|
180
|
+
for (const field of q.inputFields) {
|
|
181
|
+
res[field.fieldName] = mockAnswer(field);
|
|
182
|
+
}
|
|
183
|
+
return res;
|
|
184
|
+
}
|
|
185
|
+
case COMPONENT_TYPES.ADDRESS:
|
|
186
|
+
return {
|
|
187
|
+
[q.fieldName + '_addressLine1']: '1 Test St',
|
|
188
|
+
[q.fieldName + '_townCity']: 'Testville',
|
|
189
|
+
[q.fieldName + '_postcode']: 'TE5 5ST'
|
|
190
|
+
};
|
|
191
|
+
case COMPONENT_TYPES.UNIT_OPTION:
|
|
192
|
+
return {
|
|
193
|
+
[q.fieldName]: 'kg',
|
|
194
|
+
[q.options[0].conditional.fieldName]: 10
|
|
195
|
+
};
|
|
196
|
+
default:
|
|
197
|
+
return { [q.fieldName]: 'test' };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Build a valid & formatted answer for a question.
|
|
203
|
+
* @param {import('../src/questions/question-props.js').QuestionProps} q
|
|
204
|
+
* @returns {Object}
|
|
205
|
+
*/
|
|
206
|
+
export function mockAnswer(q) {
|
|
207
|
+
switch (q.type) {
|
|
208
|
+
case COMPONENT_TYPES.BOOLEAN:
|
|
209
|
+
return BOOLEAN_OPTIONS.YES;
|
|
210
|
+
case COMPONENT_TYPES.CHECKBOX:
|
|
211
|
+
return q.options[0].text;
|
|
212
|
+
case COMPONENT_TYPES.RADIO:
|
|
213
|
+
case COMPONENT_TYPES.SELECT:
|
|
214
|
+
return q.options[0].text;
|
|
215
|
+
case COMPONENT_TYPES.NUMBER:
|
|
216
|
+
return 1;
|
|
217
|
+
case COMPONENT_TYPES.DATE:
|
|
218
|
+
return '1 January 2025';
|
|
219
|
+
case COMPONENT_TYPES.DATE_PERIOD:
|
|
220
|
+
return 'Start: 00:00 1 January 2025<br>End: 00:00 2 January 2025';
|
|
221
|
+
case COMPONENT_TYPES.DATE_TIME:
|
|
222
|
+
return '1 January 2025<br>10:45am';
|
|
223
|
+
case COMPONENT_TYPES.TEXT_ENTRY:
|
|
224
|
+
case COMPONENT_TYPES.SINGLE_LINE_INPUT:
|
|
225
|
+
return 'test';
|
|
226
|
+
case COMPONENT_TYPES.TEXT_ENTRY_REDACT:
|
|
227
|
+
return 'secret';
|
|
228
|
+
case COMPONENT_TYPES.MULTI_FIELD_INPUT:
|
|
229
|
+
if (q.inputFields) {
|
|
230
|
+
let res = '';
|
|
231
|
+
for (const field of q.inputFields) {
|
|
232
|
+
res += mockAnswer(field) + '<br>';
|
|
233
|
+
}
|
|
234
|
+
return res;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case COMPONENT_TYPES.ADDRESS: {
|
|
238
|
+
const answer = mockAnswerBody(q);
|
|
239
|
+
return Object.values(answer).filter(Boolean).join('<br>');
|
|
240
|
+
}
|
|
241
|
+
case COMPONENT_TYPES.UNIT_OPTION:
|
|
242
|
+
return '10 kg';
|
|
243
|
+
default:
|
|
244
|
+
return 'test';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<div class="govuk-grid-row">
|
|
2
|
+
<div class="govuk-grid-column-two-thirds">
|
|
3
|
+
</div>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
<div class="govuk-grid-row">
|
|
8
|
+
<div class="govuk-grid-column-two-thirds">
|
|
9
|
+
<form action="" method="post" novalidate>
|
|
10
|
+
<input type="hidden" name="_csrf" value="">
|
|
11
|
+
<div class="govuk-form-group">
|
|
12
|
+
<fieldset class="govuk-fieldset" aria-describedby="addInsurance-hint">
|
|
13
|
+
<legend class="govuk-fieldset__legend govuk-fieldset__legend--l">
|
|
14
|
+
<h1 class="govuk-fieldset__heading">
|
|
15
|
+
Do you want to add travel insurance to your holiday?
|
|
16
|
+
</h1>
|
|
17
|
+
</legend>
|
|
18
|
+
<div id="addInsurance-hint" class="govuk-hint">
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
<div class="govuk-radios govuk-radios" data-module="govuk-radios">
|
|
22
|
+
<div class="govuk-radios__item">
|
|
23
|
+
<input class="govuk-radios__input" id="addInsurance" name="addInsurance" type="radio" value="yes" data-cy="answer-yes">
|
|
24
|
+
<label class="govuk-label govuk-radios__label" for="addInsurance">
|
|
25
|
+
Yes
|
|
26
|
+
</label>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="govuk-radios__item">
|
|
29
|
+
<input class="govuk-radios__input" id="addInsurance-2" name="addInsurance" type="radio" value="no" data-cy="answer-no">
|
|
30
|
+
<label class="govuk-label govuk-radios__label" for="addInsurance-2">
|
|
31
|
+
No
|
|
32
|
+
</label>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</fieldset>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
<button type="submit" class="govuk-button" data-module="govuk-button" data-cy="button-save-and-continue">
|
|
40
|
+
Continue
|
|
41
|
+
</button>
|
|
42
|
+
</form>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
|
|
2
|
+
<style >
|
|
3
|
+
.app-task-list {
|
|
4
|
+
padding-left: 0;
|
|
5
|
+
}
|
|
6
|
+
.app-task-list>li {
|
|
7
|
+
position: relative;
|
|
8
|
+
list-style-type: none;
|
|
9
|
+
|
|
10
|
+
}
|
|
11
|
+
.app-task-list__section .govuk-tag {
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 3px;
|
|
14
|
+
right: 0;
|
|
15
|
+
}
|
|
16
|
+
.app-task-list .govuk-summary-list {
|
|
17
|
+
border-top: 1px solid #b1b4b6;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
|
|
21
|
+
<div class="govuk-grid-row">
|
|
22
|
+
<div class="govuk-grid-column-full">
|
|
23
|
+
<dl class="govuk-summary-list">
|
|
24
|
+
<div class="govuk-summary-list__row">
|
|
25
|
+
<dt class="govuk-summary-list__key">
|
|
26
|
+
Holiday Activities
|
|
27
|
+
</dt>
|
|
28
|
+
<dd class="govuk-summary-list__value">
|
|
29
|
+
Not started
|
|
30
|
+
</dd>
|
|
31
|
+
<dd class="govuk-summary-list__actions">
|
|
32
|
+
<a class="govuk-link" href="questions/holiday-activities">Answer<span class="govuk-visually-hidden"> Which activities do you want to do on holiday?</span></a>
|
|
33
|
+
</dd>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="govuk-summary-list__row">
|
|
36
|
+
<dt class="govuk-summary-list__key">
|
|
37
|
+
Travel Insurance
|
|
38
|
+
</dt>
|
|
39
|
+
<dd class="govuk-summary-list__value">
|
|
40
|
+
Not started
|
|
41
|
+
</dd>
|
|
42
|
+
<dd class="govuk-summary-list__actions">
|
|
43
|
+
<a class="govuk-link" href="questions/add-insurance">Answer<span class="govuk-visually-hidden"> Do you want to add travel insurance to your holiday?</span></a>
|
|
44
|
+
</dd>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="govuk-summary-list__row">
|
|
47
|
+
<dt class="govuk-summary-list__key">
|
|
48
|
+
Travel Insurance Type
|
|
49
|
+
</dt>
|
|
50
|
+
<dd class="govuk-summary-list__value">
|
|
51
|
+
Not started
|
|
52
|
+
</dd>
|
|
53
|
+
<dd class="govuk-summary-list__actions">
|
|
54
|
+
<a class="govuk-link" href="questions/travel-insurance-type">Answer<span class="govuk-visually-hidden"> Which type of travel insurance would you like?</span></a>
|
|
55
|
+
</dd>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="govuk-summary-list__row">
|
|
58
|
+
<dt class="govuk-summary-list__key">
|
|
59
|
+
Holiday Destination
|
|
60
|
+
</dt>
|
|
61
|
+
<dd class="govuk-summary-list__value">
|
|
62
|
+
Not started
|
|
63
|
+
</dd>
|
|
64
|
+
<dd class="govuk-summary-list__actions">
|
|
65
|
+
<a class="govuk-link" href="questions/holiday-destination">Answer<span class="govuk-visually-hidden"> Where would you like to go on holiday?</span></a>
|
|
66
|
+
</dd>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="govuk-summary-list__row">
|
|
69
|
+
<dt class="govuk-summary-list__key">
|
|
70
|
+
Departure Date
|
|
71
|
+
</dt>
|
|
72
|
+
<dd class="govuk-summary-list__value">
|
|
73
|
+
Not started
|
|
74
|
+
</dd>
|
|
75
|
+
<dd class="govuk-summary-list__actions">
|
|
76
|
+
<a class="govuk-link" href="questions/departure-date">Answer<span class="govuk-visually-hidden"> When do you want to start your holiday?</span></a>
|
|
77
|
+
</dd>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="govuk-summary-list__row">
|
|
80
|
+
<dt class="govuk-summary-list__key">
|
|
81
|
+
Holiday Period
|
|
82
|
+
</dt>
|
|
83
|
+
<dd class="govuk-summary-list__value">
|
|
84
|
+
Not started
|
|
85
|
+
</dd>
|
|
86
|
+
<dd class="govuk-summary-list__actions">
|
|
87
|
+
<a class="govuk-link" href="questions/holiday-period">Answer<span class="govuk-visually-hidden"> What is the period of your holiday?</span></a>
|
|
88
|
+
</dd>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="govuk-summary-list__row">
|
|
91
|
+
<dt class="govuk-summary-list__key">
|
|
92
|
+
Holiday Description
|
|
93
|
+
</dt>
|
|
94
|
+
<dd class="govuk-summary-list__value">
|
|
95
|
+
Not started
|
|
96
|
+
</dd>
|
|
97
|
+
<dd class="govuk-summary-list__actions">
|
|
98
|
+
<a class="govuk-link" href="questions/holiday-description">Answer<span class="govuk-visually-hidden"> Describe your ideal holiday.</span></a>
|
|
99
|
+
</dd>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="govuk-summary-list__row">
|
|
102
|
+
<dt class="govuk-summary-list__key">
|
|
103
|
+
Secret Holiday Wish
|
|
104
|
+
</dt>
|
|
105
|
+
<dd class="govuk-summary-list__value">
|
|
106
|
+
Not started
|
|
107
|
+
</dd>
|
|
108
|
+
<dd class="govuk-summary-list__actions">
|
|
109
|
+
<a class="govuk-link" href="questions/secret-wish">Answer<span class="govuk-visually-hidden"> Share a secret wish for your holiday (will be redacted).</span></a>
|
|
110
|
+
</dd>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="govuk-summary-list__row">
|
|
113
|
+
<dt class="govuk-summary-list__key">
|
|
114
|
+
Travel Class
|
|
115
|
+
</dt>
|
|
116
|
+
<dd class="govuk-summary-list__value">
|
|
117
|
+
Not started
|
|
118
|
+
</dd>
|
|
119
|
+
<dd class="govuk-summary-list__actions">
|
|
120
|
+
<a class="govuk-link" href="questions/travel-class">Answer<span class="govuk-visually-hidden"> Which class do you want to travel in?</span></a>
|
|
121
|
+
</dd>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="govuk-summary-list__row">
|
|
124
|
+
<dt class="govuk-summary-list__key">
|
|
125
|
+
Holiday Arrival
|
|
126
|
+
</dt>
|
|
127
|
+
<dd class="govuk-summary-list__value">
|
|
128
|
+
Not started
|
|
129
|
+
</dd>
|
|
130
|
+
<dd class="govuk-summary-list__actions">
|
|
131
|
+
<a class="govuk-link" href="questions/holiday-arrival">Answer<span class="govuk-visually-hidden"> When would you like to arrive at your destination?</span></a>
|
|
132
|
+
</dd>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="govuk-summary-list__row">
|
|
135
|
+
<dt class="govuk-summary-list__key">
|
|
136
|
+
Favourite Holiday Snack
|
|
137
|
+
</dt>
|
|
138
|
+
<dd class="govuk-summary-list__value">
|
|
139
|
+
Not started
|
|
140
|
+
</dd>
|
|
141
|
+
<dd class="govuk-summary-list__actions">
|
|
142
|
+
<a class="govuk-link" href="questions/holiday-snack">Answer<span class="govuk-visually-hidden"> What is your favourite snack to eat on holiday?</span></a>
|
|
143
|
+
</dd>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="govuk-summary-list__row">
|
|
146
|
+
<dt class="govuk-summary-list__key">
|
|
147
|
+
Travel Companions
|
|
148
|
+
</dt>
|
|
149
|
+
<dd class="govuk-summary-list__value">
|
|
150
|
+
Not started
|
|
151
|
+
</dd>
|
|
152
|
+
<dd class="govuk-summary-list__actions">
|
|
153
|
+
<a class="govuk-link" href="questions/companions">Answer<span class="govuk-visually-hidden"> Enter the names and ages of your travel companions.</span></a>
|
|
154
|
+
</dd>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="govuk-summary-list__row">
|
|
157
|
+
<dt class="govuk-summary-list__key">
|
|
158
|
+
Number of Nights
|
|
159
|
+
</dt>
|
|
160
|
+
<dd class="govuk-summary-list__value">
|
|
161
|
+
Not started
|
|
162
|
+
</dd>
|
|
163
|
+
<dd class="govuk-summary-list__actions">
|
|
164
|
+
<a class="govuk-link" href="questions/nights">Answer<span class="govuk-visually-hidden"> How many nights will you stay on holiday?</span></a>
|
|
165
|
+
</dd>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="govuk-summary-list__row">
|
|
168
|
+
<dt class="govuk-summary-list__key">
|
|
169
|
+
Hotel Address
|
|
170
|
+
</dt>
|
|
171
|
+
<dd class="govuk-summary-list__value">
|
|
172
|
+
Not started
|
|
173
|
+
</dd>
|
|
174
|
+
<dd class="govuk-summary-list__actions">
|
|
175
|
+
<a class="govuk-link" href="questions/hotel-address">Answer<span class="govuk-visually-hidden"> What is the address of your hotel?</span></a>
|
|
176
|
+
</dd>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="govuk-summary-list__row">
|
|
179
|
+
<dt class="govuk-summary-list__key">
|
|
180
|
+
Luggage Weight
|
|
181
|
+
</dt>
|
|
182
|
+
<dd class="govuk-summary-list__value">
|
|
183
|
+
Not started
|
|
184
|
+
</dd>
|
|
185
|
+
<dd class="govuk-summary-list__actions">
|
|
186
|
+
<a class="govuk-link" href="questions/luggage-weight-units">Answer<span class="govuk-visually-hidden"> Select your luggage weight and unit.</span></a>
|
|
187
|
+
</dd>
|
|
188
|
+
</div>
|
|
189
|
+
</dl>
|
|
190
|
+
|
|
191
|
+
<form action method="post">
|
|
192
|
+
<input type="hidden" name="_csrf" value="">
|
|
193
|
+
<button type="submit" class="govuk-button" data-module="govuk-button">
|
|
194
|
+
Accept & Submit
|
|
195
|
+
</button>
|
|
196
|
+
</form>
|
|
197
|
+
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
|