@planning-inspectorate/dynamic-forms 3.3.3 → 3.4.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 (272) hide show
  1. package/.azure/pipelines/azure-pipelines-pr.yml +4 -0
  2. package/.azure/pipelines/release.yml +4 -0
  3. package/.gitattributes +2 -0
  4. package/README.md +20 -9
  5. package/package.json +13 -5
  6. package/src/components/address/question.js +1 -10
  7. package/src/components/boolean/question.js +1 -1
  8. package/src/components/checkbox/question.js +2 -6
  9. package/src/components/date/question.js +1 -8
  10. package/src/components/date-period/question.js +1 -8
  11. package/src/components/email/question.js +4 -4
  12. package/src/components/manage-list/question.test.js +4 -19
  13. package/src/components/multi-field-input/question.js +3 -3
  14. package/src/components/multi-field-input/question.test.js +1 -1
  15. package/src/components/number-entry/question.js +1 -5
  16. package/src/components/radio/question.js +1 -1
  17. package/src/components/select/question.js +1 -1
  18. package/src/components/single-line-input/question.js +0 -8
  19. package/src/components/text-entry/question.js +4 -4
  20. package/src/components/text-entry-redact/index.njk +15 -11
  21. package/src/components/text-entry-redact/question.js +6 -16
  22. package/src/components/unit-option-entry/question.js +4 -11
  23. package/src/components/utils/component-types.js +18 -0
  24. package/src/index.js +72 -25
  25. package/src/middleware/redirect-to-unanswered-question.test.js +1 -1
  26. package/src/questions/options-question.js +5 -12
  27. package/src/questions/question-types.d.ts +3 -3
  28. package/src/questions/question.js +6 -6
  29. package/src/questions/questions.js +1 -1
  30. package/test/check-your-answers.test.js +3 -8
  31. package/test/conditional-journey.test.js +431 -0
  32. package/test/questions.test.js +15 -28
  33. package/test/snapshots/secret-wish.html +15 -11
  34. package/test/utils/question-test-utils.js +52 -4
  35. package/test/utils/test-server.js +1 -0
  36. package/test/utils/utils.js +16 -1
  37. package/tsconfig.json +9 -1
  38. package/types/commitlint.config.d.ts +7 -0
  39. package/types/commitlint.config.d.ts.map +1 -0
  40. package/types/eslint.config.d.ts +95 -0
  41. package/types/eslint.config.d.ts.map +1 -0
  42. package/types/src/components/address/question.d.ts +31 -0
  43. package/types/src/components/address/question.d.ts.map +1 -0
  44. package/types/src/components/address/question.test.d.ts +2 -0
  45. package/types/src/components/address/question.test.d.ts.map +1 -0
  46. package/types/src/components/boolean/question.d.ts +47 -0
  47. package/types/src/components/boolean/question.d.ts.map +1 -0
  48. package/types/src/components/boolean/question.test.d.ts +2 -0
  49. package/types/src/components/boolean/question.test.d.ts.map +1 -0
  50. package/types/src/components/checkbox/question.d.ts +49 -0
  51. package/types/src/components/checkbox/question.d.ts.map +1 -0
  52. package/types/src/components/checkbox/question.test.d.ts +2 -0
  53. package/types/src/components/checkbox/question.test.d.ts.map +1 -0
  54. package/types/src/components/date/question.d.ts +22 -0
  55. package/types/src/components/date/question.d.ts.map +1 -0
  56. package/types/src/components/date/question.test.d.ts +2 -0
  57. package/types/src/components/date/question.test.d.ts.map +1 -0
  58. package/types/src/components/date-period/question.d.ts +29 -0
  59. package/types/src/components/date-period/question.d.ts.map +1 -0
  60. package/types/src/components/date-period/question.test.d.ts +2 -0
  61. package/types/src/components/date-period/question.test.d.ts.map +1 -0
  62. package/types/src/components/date-time/question.d.ts +24 -0
  63. package/types/src/components/date-time/question.d.ts.map +1 -0
  64. package/types/src/components/date-time/question.test.d.ts +2 -0
  65. package/types/src/components/date-time/question.test.d.ts.map +1 -0
  66. package/types/src/components/email/question.d.ts +30 -0
  67. package/types/src/components/email/question.d.ts.map +1 -0
  68. package/types/src/components/email/question.test.d.ts +2 -0
  69. package/types/src/components/email/question.test.d.ts.map +1 -0
  70. package/types/src/components/identifier/question.d.ts +15 -0
  71. package/types/src/components/identifier/question.d.ts.map +1 -0
  72. package/types/src/components/identifier/question.test.d.ts +2 -0
  73. package/types/src/components/identifier/question.test.d.ts.map +1 -0
  74. package/types/src/components/manage-list/manage-list-section.d.ts +16 -0
  75. package/types/src/components/manage-list/manage-list-section.d.ts.map +1 -0
  76. package/types/src/components/manage-list/manage-list-section.test.d.ts +2 -0
  77. package/types/src/components/manage-list/manage-list-section.test.d.ts.map +1 -0
  78. package/types/src/components/manage-list/question.d.ts +41 -0
  79. package/types/src/components/manage-list/question.d.ts.map +1 -0
  80. package/types/src/components/manage-list/question.test.d.ts +2 -0
  81. package/types/src/components/manage-list/question.test.d.ts.map +1 -0
  82. package/types/src/components/manage-list/utils.d.ts +18 -0
  83. package/types/src/components/manage-list/utils.d.ts.map +1 -0
  84. package/types/src/components/manage-list/utils.test.d.ts +2 -0
  85. package/types/src/components/manage-list/utils.test.d.ts.map +1 -0
  86. package/types/src/components/multi-field-input/question.d.ts +106 -0
  87. package/types/src/components/multi-field-input/question.d.ts.map +1 -0
  88. package/types/src/components/multi-field-input/question.test.d.ts +2 -0
  89. package/types/src/components/multi-field-input/question.test.d.ts.map +1 -0
  90. package/types/src/components/number-entry/question.d.ts +37 -0
  91. package/types/src/components/number-entry/question.d.ts.map +1 -0
  92. package/types/src/components/radio/question.d.ts +53 -0
  93. package/types/src/components/radio/question.d.ts.map +1 -0
  94. package/types/src/components/radio/question.test.d.ts +2 -0
  95. package/types/src/components/radio/question.test.d.ts.map +1 -0
  96. package/types/src/components/select/question.d.ts +51 -0
  97. package/types/src/components/select/question.d.ts.map +1 -0
  98. package/types/src/components/select/question.test.d.ts +2 -0
  99. package/types/src/components/select/question.test.d.ts.map +1 -0
  100. package/types/src/components/single-line-input/question.d.ts +18 -0
  101. package/types/src/components/single-line-input/question.d.ts.map +1 -0
  102. package/types/src/components/single-line-input/question.test.d.ts +2 -0
  103. package/types/src/components/single-line-input/question.test.d.ts.map +1 -0
  104. package/types/src/components/text-entry/question.d.ts +34 -0
  105. package/types/src/components/text-entry/question.d.ts.map +1 -0
  106. package/types/src/components/text-entry/question.test.d.ts +2 -0
  107. package/types/src/components/text-entry/question.test.d.ts.map +1 -0
  108. package/types/src/components/text-entry-redact/question.d.ts +42 -0
  109. package/types/src/components/text-entry-redact/question.d.ts.map +1 -0
  110. package/types/src/components/text-entry-redact/question.test.d.ts +2 -0
  111. package/types/src/components/text-entry-redact/question.test.d.ts.map +1 -0
  112. package/types/src/components/unit-option-entry/question.d.ts +83 -0
  113. package/types/src/components/unit-option-entry/question.d.ts.map +1 -0
  114. package/types/src/components/unit-option-entry/question.test.d.ts +2 -0
  115. package/types/src/components/unit-option-entry/question.test.d.ts.map +1 -0
  116. package/types/src/components/utils/component-types.d.ts +19 -0
  117. package/types/src/components/utils/component-types.d.ts.map +1 -0
  118. package/types/src/components/utils/persisted-number-answer.d.ts +2 -0
  119. package/types/src/components/utils/persisted-number-answer.d.ts.map +1 -0
  120. package/types/src/components/utils/question-has-answer.d.ts +17 -0
  121. package/types/src/components/utils/question-has-answer.d.ts.map +1 -0
  122. package/types/src/components/utils/question-has-answer.test.d.ts +2 -0
  123. package/types/src/components/utils/question-has-answer.test.d.ts.map +1 -0
  124. package/types/src/components/utils/question-utils.d.ts +15 -0
  125. package/types/src/components/utils/question-utils.d.ts.map +1 -0
  126. package/types/src/components/utils/question-utils.test.d.ts +2 -0
  127. package/types/src/components/utils/question-utils.test.d.ts.map +1 -0
  128. package/types/src/controller.d.ts +78 -0
  129. package/types/src/controller.d.ts.map +1 -0
  130. package/types/src/controller.test.d.ts +2 -0
  131. package/types/src/controller.test.d.ts.map +1 -0
  132. package/types/src/index.d.ts +54 -0
  133. package/types/src/index.d.ts.map +1 -0
  134. package/types/src/journey/journey-response.d.ts +32 -0
  135. package/types/src/journey/journey-response.d.ts.map +1 -0
  136. package/types/src/journey/journey-response.test.d.ts +2 -0
  137. package/types/src/journey/journey-response.test.d.ts.map +1 -0
  138. package/types/src/journey/journey.d.ts +144 -0
  139. package/types/src/journey/journey.d.ts.map +1 -0
  140. package/types/src/journey/journey.test.d.ts +2 -0
  141. package/types/src/journey/journey.test.d.ts.map +1 -0
  142. package/types/src/lib/address-utils.d.ts +6 -0
  143. package/types/src/lib/address-utils.d.ts.map +1 -0
  144. package/types/src/lib/address-utils.test.d.ts +2 -0
  145. package/types/src/lib/address-utils.test.d.ts.map +1 -0
  146. package/types/src/lib/address.d.ts +43 -0
  147. package/types/src/lib/address.d.ts.map +1 -0
  148. package/types/src/lib/address.test.d.ts +2 -0
  149. package/types/src/lib/address.test.d.ts.map +1 -0
  150. package/types/src/lib/date-utils.d.ts +60 -0
  151. package/types/src/lib/date-utils.d.ts.map +1 -0
  152. package/types/src/lib/date-utils.test.d.ts +2 -0
  153. package/types/src/lib/date-utils.test.d.ts.map +1 -0
  154. package/types/src/lib/session-answer-store.d.ts +84 -0
  155. package/types/src/lib/session-answer-store.d.ts.map +1 -0
  156. package/types/src/lib/session-answer-store.test.d.ts +2 -0
  157. package/types/src/lib/session-answer-store.test.d.ts.map +1 -0
  158. package/types/src/lib/utils.d.ts +19 -0
  159. package/types/src/lib/utils.d.ts.map +1 -0
  160. package/types/src/lib/utils.test.d.ts +2 -0
  161. package/types/src/lib/utils.test.d.ts.map +1 -0
  162. package/types/src/lib/uuid.d.ts +7 -0
  163. package/types/src/lib/uuid.d.ts.map +1 -0
  164. package/types/src/lib/uuid.test.d.ts +2 -0
  165. package/types/src/lib/uuid.test.d.ts.map +1 -0
  166. package/types/src/middleware/build-get-journey.d.ts +6 -0
  167. package/types/src/middleware/build-get-journey.d.ts.map +1 -0
  168. package/types/src/middleware/redirect-to-unanswered-question.d.ts +12 -0
  169. package/types/src/middleware/redirect-to-unanswered-question.d.ts.map +1 -0
  170. package/types/src/middleware/redirect-to-unanswered-question.test.d.ts +2 -0
  171. package/types/src/middleware/redirect-to-unanswered-question.test.d.ts.map +1 -0
  172. package/types/src/questions/create-questions.d.ts +21 -0
  173. package/types/src/questions/create-questions.d.ts.map +1 -0
  174. package/types/src/questions/options-question.d.ts +74 -0
  175. package/types/src/questions/options-question.d.ts.map +1 -0
  176. package/types/src/questions/options-question.test.d.ts +2 -0
  177. package/types/src/questions/options-question.test.d.ts.map +1 -0
  178. package/types/src/questions/question.d.ts +274 -0
  179. package/types/src/questions/question.d.ts.map +1 -0
  180. package/types/src/questions/question.test.d.ts +2 -0
  181. package/types/src/questions/question.test.d.ts.map +1 -0
  182. package/types/src/questions/questions.d.ts +3 -0
  183. package/types/src/questions/questions.d.ts.map +1 -0
  184. package/types/src/section.d.ts +107 -0
  185. package/types/src/section.d.ts.map +1 -0
  186. package/types/src/section.test.d.ts +2 -0
  187. package/types/src/section.test.d.ts.map +1 -0
  188. package/types/src/validator/address-validator.d.ts +41 -0
  189. package/types/src/validator/address-validator.d.ts.map +1 -0
  190. package/types/src/validator/address-validator.test.d.ts +2 -0
  191. package/types/src/validator/address-validator.test.d.ts.map +1 -0
  192. package/types/src/validator/base-validator.d.ts +11 -0
  193. package/types/src/validator/base-validator.d.ts.map +1 -0
  194. package/types/src/validator/base-validator.test.d.ts +2 -0
  195. package/types/src/validator/base-validator.test.d.ts.map +1 -0
  196. package/types/src/validator/conditional-required-validator.d.ts +24 -0
  197. package/types/src/validator/conditional-required-validator.d.ts.map +1 -0
  198. package/types/src/validator/conditional-required-validator.test.d.ts +2 -0
  199. package/types/src/validator/conditional-required-validator.test.d.ts.map +1 -0
  200. package/types/src/validator/confirmation-checkbox-validator.d.ts +27 -0
  201. package/types/src/validator/confirmation-checkbox-validator.d.ts.map +1 -0
  202. package/types/src/validator/confirmation-checkbox-validator.test.d.ts +2 -0
  203. package/types/src/validator/confirmation-checkbox-validator.test.d.ts.map +1 -0
  204. package/types/src/validator/coordinates-validator.d.ts +22 -0
  205. package/types/src/validator/coordinates-validator.d.ts.map +1 -0
  206. package/types/src/validator/coordinates-validator.test.d.ts +2 -0
  207. package/types/src/validator/coordinates-validator.test.d.ts.map +1 -0
  208. package/types/src/validator/date-period-validator.d.ts +63 -0
  209. package/types/src/validator/date-period-validator.d.ts.map +1 -0
  210. package/types/src/validator/date-period-validator.test.d.ts +2 -0
  211. package/types/src/validator/date-period-validator.test.d.ts.map +1 -0
  212. package/types/src/validator/date-time-validator.d.ts +26 -0
  213. package/types/src/validator/date-time-validator.d.ts.map +1 -0
  214. package/types/src/validator/date-time-validator.test.d.ts +2 -0
  215. package/types/src/validator/date-time-validator.test.d.ts.map +1 -0
  216. package/types/src/validator/date-validator.d.ts +48 -0
  217. package/types/src/validator/date-validator.d.ts.map +1 -0
  218. package/types/src/validator/date-validator.test.d.ts +2 -0
  219. package/types/src/validator/date-validator.test.d.ts.map +1 -0
  220. package/types/src/validator/document-upload-validator.d.ts +10 -0
  221. package/types/src/validator/document-upload-validator.d.ts.map +1 -0
  222. package/types/src/validator/document-upload-validator.test.d.ts +2 -0
  223. package/types/src/validator/document-upload-validator.test.d.ts.map +1 -0
  224. package/types/src/validator/email-validator.d.ts +73 -0
  225. package/types/src/validator/email-validator.d.ts.map +1 -0
  226. package/types/src/validator/email-validator.test.d.ts +2 -0
  227. package/types/src/validator/email-validator.test.d.ts.map +1 -0
  228. package/types/src/validator/multi-field-input-validator.d.ts +70 -0
  229. package/types/src/validator/multi-field-input-validator.d.ts.map +1 -0
  230. package/types/src/validator/multi-field-input-validator.test.d.ts +2 -0
  231. package/types/src/validator/multi-field-input-validator.test.d.ts.map +1 -0
  232. package/types/src/validator/numeric-validator.d.ts +49 -0
  233. package/types/src/validator/numeric-validator.d.ts.map +1 -0
  234. package/types/src/validator/numeric-validator.test.d.ts +2 -0
  235. package/types/src/validator/numeric-validator.test.d.ts.map +1 -0
  236. package/types/src/validator/required-validator.d.ts +22 -0
  237. package/types/src/validator/required-validator.d.ts.map +1 -0
  238. package/types/src/validator/required-validator.test.d.ts +2 -0
  239. package/types/src/validator/required-validator.test.d.ts.map +1 -0
  240. package/types/src/validator/same-answer-validator.d.ts +18 -0
  241. package/types/src/validator/same-answer-validator.d.ts.map +1 -0
  242. package/types/src/validator/same-answer-validator.test.d.ts +2 -0
  243. package/types/src/validator/same-answer-validator.test.d.ts.map +1 -0
  244. package/types/src/validator/string-validator.d.ts +58 -0
  245. package/types/src/validator/string-validator.d.ts.map +1 -0
  246. package/types/src/validator/string-validator.test.d.ts +2 -0
  247. package/types/src/validator/string-validator.test.d.ts.map +1 -0
  248. package/types/src/validator/unit-option-entry-validator.d.ts +40 -0
  249. package/types/src/validator/unit-option-entry-validator.d.ts.map +1 -0
  250. package/types/src/validator/unit-option-entry-validator.test.d.ts +2 -0
  251. package/types/src/validator/unit-option-entry-validator.test.d.ts.map +1 -0
  252. package/types/src/validator/valid-option-validator.d.ts +22 -0
  253. package/types/src/validator/valid-option-validator.d.ts.map +1 -0
  254. package/types/src/validator/valid-option-validator.test.d.ts +2 -0
  255. package/types/src/validator/valid-option-validator.test.d.ts.map +1 -0
  256. package/types/src/validator/validation-error-handler.d.ts +20 -0
  257. package/types/src/validator/validation-error-handler.d.ts.map +1 -0
  258. package/types/src/validator/validation-error-handler.test.d.ts +2 -0
  259. package/types/src/validator/validation-error-handler.test.d.ts.map +1 -0
  260. package/types/src/validator/validator.d.ts +5 -0
  261. package/types/src/validator/validator.d.ts.map +1 -0
  262. package/types/src/validator/validator.test.d.ts +2 -0
  263. package/types/src/validator/validator.test.d.ts.map +1 -0
  264. package/types/test/mock/journey.d.ts +3 -0
  265. package/types/test/mock/journey.d.ts.map +1 -0
  266. package/types/test/mock/uuid.d.ts +7 -0
  267. package/types/test/mock/uuid.d.ts.map +1 -0
  268. package/types/test/utils/nunjucks.d.ts +2 -0
  269. package/types/test/utils/nunjucks.d.ts.map +1 -0
  270. package/types/test/utils/utils.d.ts +32 -0
  271. package/types/test/utils/utils.d.ts.map +1 -0
  272. package/src/lib/address.d.ts +0 -7
package/src/index.js CHANGED
@@ -1,25 +1,72 @@
1
- /**
2
- * @typedef {import('./questions/question-props.js').QuestionTypes} QuestionTypes
3
- */
4
-
5
- /**
6
- * @type {Readonly<{CHECKBOX: QuestionTypes, BOOLEAN: QuestionTypes, RADIO: QuestionTypes, DATE: QuestionTypes, DATE_PERIOD: QuestionTypes, TEXT_ENTRY: QuestionTypes, TEXT_ENTRY_REDACT: QuestionTypes, SELECT: QuestionTypes, SINGLE_LINE_INPUT: QuestionTypes, MULTI_FIELD_INPUT: QuestionTypes, NUMBER: QuestionTypes, ADDRESS: QuestionTypes, UNIT_OPTION: QuestionTypes, EMAIL: QuestionTypes, MANAGE_LIST: QuestionTypes}>}
7
- */
8
- export const COMPONENT_TYPES = Object.freeze({
9
- CHECKBOX: 'checkbox',
10
- BOOLEAN: 'boolean',
11
- RADIO: 'radio',
12
- DATE: 'date',
13
- DATE_PERIOD: 'date-period',
14
- DATE_TIME: 'date-time',
15
- TEXT_ENTRY: 'text-entry',
16
- TEXT_ENTRY_REDACT: 'text-entry-redact',
17
- SELECT: 'select',
18
- SINGLE_LINE_INPUT: 'single-line-input',
19
- MULTI_FIELD_INPUT: 'multi-field-input',
20
- NUMBER: 'number',
21
- ADDRESS: 'site-address',
22
- UNIT_OPTION: 'unit-option',
23
- EMAIL: 'email',
24
- MANAGE_LIST: 'manage-list'
25
- });
1
+ // Components
2
+ import AddressQuestion from './components/address/question.js';
3
+ import DateValidator from './validator/date-validator.js';
4
+ import EmailValidator from './validator/email-validator.js';
5
+ import StringValidator from './validator/string-validator.js';
6
+
7
+ export { AddressQuestion };
8
+ export * from './components/boolean/question.js';
9
+ export * from './components/checkbox/question.js';
10
+ export * from './components/date/question.js';
11
+ export * from './components/date-period/question.js';
12
+ export * from './components/multi-field-input/question.js';
13
+ export * from './components/number-entry/question.js';
14
+ export * from './components/radio/question.js';
15
+ export * from './components/select/question.js';
16
+ export * from './components/single-line-input/question.js';
17
+ export * from './components/text-entry/question.js';
18
+ export * from './components/text-entry-redact/question.js';
19
+ export * from './components/unit-option-entry/question.js';
20
+ export * from './components/utils/persisted-number-answer.js';
21
+ export * from './components/utils/question-has-answer.js';
22
+ export * from './components/utils/question-utils.js';
23
+ export * from './components/utils/component-types.js';
24
+
25
+ // Controller
26
+ export * from './controller.js';
27
+
28
+ // Journey
29
+ export { Journey } from './journey/journey.js';
30
+ export { JourneyResponse } from './journey/journey-response.js';
31
+
32
+ // lib
33
+ export * from './lib/address.js';
34
+ export * from './lib/address-utils.js';
35
+ export * from './lib/date-utils.js';
36
+ export * from './lib/session-answer-store.js';
37
+ export * from './lib/utils.js';
38
+
39
+ // middleware
40
+ export * from './middleware/build-get-journey.js';
41
+ export * from './middleware/redirect-to-unanswered-question.js';
42
+
43
+ // Questions
44
+ export { Question } from './questions/question.js';
45
+ export { createQuestions } from './questions/create-questions.js';
46
+ export * from './questions/options-question.js';
47
+ export { questionClasses } from './questions/questions.js';
48
+
49
+ // Section
50
+ export { Section } from './section.js';
51
+
52
+ // validators
53
+ export * from './validator/address-validator.js';
54
+ export * from './validator/base-validator.js';
55
+ export * from './validator/conditional-required-validator.js';
56
+ export * from './validator/confirmation-checkbox-validator.js';
57
+ export * from './validator/coordinates-validator.js';
58
+ export * from './validator/date-period-validator.js';
59
+ export * from './validator/date-time-validator.js';
60
+ export { DateValidator };
61
+ export { EmailValidator };
62
+ export * from './validator/document-upload-validator.js';
63
+ export * from './validator/multi-field-input-validator.js';
64
+ export * from './validator/numeric-validator.js';
65
+ export { default as RequiredValidator } from './validator/required-validator.js';
66
+ export * from './validator/same-answer-validator.js';
67
+ export { StringValidator };
68
+ export * from './validator/unit-option-entry-validator.js';
69
+ export * from './validator/valid-option-validator.js';
70
+ export * from './validator/validation-error-handler.js';
71
+ export { default as validate } from './validator/validator.js';
72
+ export * from './validator/validator.js';
@@ -5,7 +5,7 @@ import { redirectToUnansweredQuestion } from './redirect-to-unanswered-question.
5
5
  import { JourneyResponse } from '../journey/journey-response.js';
6
6
  import { Journey } from '../journey/journey.js';
7
7
  import { Section } from '../section.js';
8
- import { COMPONENT_TYPES } from '../index.js';
8
+ import { COMPONENT_TYPES } from '../components/utils/component-types.js';
9
9
  import { questionHasAnswer } from '../components/utils/question-has-answer.js';
10
10
 
11
11
  describe('redirectToUnansweredQuestion Middleware', () => {
@@ -5,13 +5,6 @@ import { getConditionalFieldName } from '../components/utils/question-utils.js';
5
5
 
6
6
  const defaultOptionJoinString = ',';
7
7
 
8
- /**
9
- * @typedef {import('./question').QuestionViewModel} QuestionViewModel
10
- * @typedef {import('../journey/journey.js').Journey} Journey
11
- * @typedef {import('../journey/journey-response.js').JourneyResponse} JourneyResponse
12
- * @typedef {import('../section.js').Section} Section
13
- */
14
-
15
8
  /**
16
9
  * @typedef {{
17
10
  * text: string;
@@ -37,7 +30,7 @@ const defaultOptionJoinString = ',';
37
30
  */
38
31
 
39
32
  /**
40
- * @typedef {QuestionViewModel & { question: { options: Option[] } }} OptionsViewModel
33
+ * @typedef {import('./question').QuestionViewModel & { question: { options: Option[] } }} OptionsViewModel
41
34
  */
42
35
 
43
36
  export default class OptionsQuestion extends Question {
@@ -65,12 +58,12 @@ export default class OptionsQuestion extends Question {
65
58
 
66
59
  /**
67
60
  * gets the view model for this question
68
- * @param {Section} section - the current section
69
- * @param {Journey} journey - the journey we are in
61
+ * @param {import('#section').Section} section - the current section
62
+ * @param {import('#journey').Journey} journey - the journey we are in
70
63
  * @param {Record<string, unknown>} [customViewData] additional data to send to view
71
64
  * @param {Record<string, unknown>} [payload]
72
65
  * @param {import('#src/questions/question-types.d.ts').PrepQuestionForRenderingOptions} options
73
- * @returns {QuestionViewModel}
66
+ * @returns {import('./question').QuestionViewModel} QuestionViewModel
74
67
  */
75
68
  prepQuestionForRendering(section, journey, customViewData, payload, options) {
76
69
  const viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload, options);
@@ -125,7 +118,7 @@ export default class OptionsQuestion extends Question {
125
118
  /**
126
119
  * Get the data to save from the request, returns an object of answers
127
120
  * @param {import('express').Request} req
128
- * @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
121
+ * @param {import('../journey/journey-response.js').JourneyResponse} journeyResponse - current journey response, modified with the new answers
129
122
  * @returns {Promise<{ answers: Record<string, unknown> }>}
130
123
  */ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
131
124
  async getDataToSave(req, journeyResponse) {
@@ -1,7 +1,7 @@
1
- import BaseValidator from '../validator/base-validator';
2
- import { JourneyResponse } from '../journey/journey-response';
3
1
  import { RouteParams } from '#src/journey/journey-types.d.ts';
4
2
  import ManageListQuestion from '#src/components/manage-list/question.js';
3
+ import BaseValidator from '../validator/base-validator.js';
4
+ import { JourneyResponse } from '../journey/journey-response.js';
5
5
 
6
6
  export interface QuestionParameters {
7
7
  title: string;
@@ -22,7 +22,7 @@ export interface QuestionParameters {
22
22
  // override the action link for this question
23
23
  actionLink?: ActionLink;
24
24
  // static view data for this question
25
- viewData?: Object<string, any>;
25
+ viewData?: Record<string, any>;
26
26
  }
27
27
 
28
28
  export interface ActionLink {
@@ -6,12 +6,12 @@ import MultiFieldInputValidator from '../validator/multi-field-input-validator.j
6
6
  import { answerObjectForManageList } from '#src/components/manage-list/utils.js';
7
7
 
8
8
  /**
9
- * @typedef {import('../validator/base-validator.js')} BaseValidator
10
- * @typedef {import('../journey/journey.js').Journey} Journey
11
- * @typedef {import('../journey/journey-response.js').JourneyResponse} JourneyResponse
12
- * @typedef {import('../section.js').Section} Section
13
- * @typedef {import('../controller.js').ActionView} ActionView
14
- * @typedef {import('./question-types.js').ActionLink} ActionLink
9
+ * @import {BaseValidator} from "#base-validator"
10
+ * @import {Journey} from "#journey"
11
+ * @import {JourneyResponse} from "#journey-response"
12
+ * @import {Section} from "#section"
13
+ * @import {ActionView} from "../controller.js"
14
+ * @import {ActionLink} from "./question-types.js"
15
15
  */
16
16
 
17
17
  /**
@@ -12,7 +12,7 @@ import NumberEntryQuestion from '../components/number-entry/question.js';
12
12
  import UnitOptionEntryQuestion from '../components/unit-option-entry/question.js';
13
13
  import EmailQuestion from '../components/email/question.js';
14
14
 
15
- import { COMPONENT_TYPES } from '../index.js';
15
+ import { COMPONENT_TYPES } from '../components/utils/component-types.js';
16
16
  import TextEntryRedactQuestion from '../components/text-entry-redact/question.js';
17
17
  import DateTimeQuestion from '../components/date-time/question.js';
18
18
  import ManageListQuestion from '../components/manage-list/question.js';
@@ -1,8 +1,7 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'assert';
3
3
  import { manageListQuestions, questionsInOrder } from './questions.js';
4
- import path from 'path';
5
- import { escapeForRegExp, snapshotsDir } from './utils/utils.js';
4
+ import { assertSnapshot, escapeForRegExp } from './utils/utils.js';
6
5
  import { createAppWithQuestions, mockAnswer, mockAnswerBody } from './utils/question-test-utils.js';
7
6
  import { COMPONENT_TYPES } from '#src/index.js';
8
7
 
@@ -30,9 +29,7 @@ describe('check-your-answers', () => {
30
29
  new RegExp(`<a[^>]* href="questions/${q.url}">\\s*Answer\\s*<span[^>]*>\\s*${escapedQ}\\s*</span></a>`, 'i')
31
30
  );
32
31
  }
33
- ctx.assert.fileSnapshot(text, path.join(snapshotsDir(), 'check-your-answers-not-started.html'), {
34
- serializers: [(v) => v]
35
- });
32
+ assertSnapshot(ctx, text, 'check-your-answers-not-started.html');
36
33
  });
37
34
 
38
35
  it(`should render all question answers`, async (ctx) => {
@@ -80,8 +77,6 @@ describe('check-your-answers', () => {
80
77
  new RegExp(`<a[^>]* href="questions/${q.url}">\\s*Change\\s*<span[^>]*>\\s*${escapedQ}\\s*</span></a>`, 'i')
81
78
  );
82
79
  }
83
- ctx.assert.fileSnapshot(text, path.join(snapshotsDir(), 'check-your-answers-with-answers.html'), {
84
- serializers: [(v) => v]
85
- });
80
+ assertSnapshot(ctx, text, 'check-your-answers-with-answers.html');
86
81
  });
87
82
  });
@@ -0,0 +1,431 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'assert';
3
+ import { Section } from '#src/section.js';
4
+ import { Journey } from '#src/journey/journey.js';
5
+ import { COMPONENT_TYPES } from '#src/index.js';
6
+ import { createQuestions } from '#src/questions/create-questions.js';
7
+ import { questionClasses } from '#src/questions/questions.js';
8
+ import { whenQuestionHasAnswer } from '#src/components/utils/question-has-answer.js';
9
+ import { BOOLEAN_OPTIONS } from '#src/components/boolean/question.js';
10
+ import { createAppWithQuestions, renderQuestionCheck, postAnswer } from '#test/utils/question-test-utils.js';
11
+
12
+ const JOURNEY_ID = 'conditional-journey';
13
+
14
+ /**
15
+ * Question props for conditional journey tests
16
+ * @type {Record<string, import('#src/questions/question-props.js').QuestionProps>}
17
+ */
18
+ const conditionalQuestionProps = {
19
+ // Section 1: Insurance (simple + chained conditions)
20
+ wantsInsurance: {
21
+ type: COMPONENT_TYPES.BOOLEAN,
22
+ title: 'Want Insurance',
23
+ question: 'Do you want to add travel insurance?',
24
+ fieldName: 'wantsInsurance',
25
+ url: 'wants-insurance',
26
+ label: 'Add insurance?'
27
+ },
28
+ insuranceLevel: {
29
+ type: COMPONENT_TYPES.RADIO,
30
+ title: 'Insurance Level',
31
+ question: 'What level of insurance do you want?',
32
+ fieldName: 'insuranceLevel',
33
+ url: 'insurance-level',
34
+ label: 'Select an insurance level',
35
+ options: [
36
+ { value: 'basic', text: 'Basic' },
37
+ { value: 'premium', text: 'Premium' }
38
+ ]
39
+ },
40
+ premiumBenefits: {
41
+ type: COMPONENT_TYPES.CHECKBOX,
42
+ title: 'Premium Benefits',
43
+ question: 'Which premium benefits do you want?',
44
+ fieldName: 'premiumBenefits',
45
+ url: 'premium-benefits',
46
+ label: 'Select all that apply',
47
+ options: [
48
+ { value: 'cancellation', text: 'Cancellation cover' },
49
+ { value: 'medical', text: 'Medical cover' },
50
+ { value: 'luggage', text: 'Luggage cover' }
51
+ ]
52
+ },
53
+ // Section 2: Trip (leads to section condition)
54
+ tripType: {
55
+ type: COMPONENT_TYPES.RADIO,
56
+ title: 'Trip Type',
57
+ question: 'What type of trip are you planning?',
58
+ fieldName: 'tripType',
59
+ url: 'trip-type',
60
+ label: 'Select a trip type',
61
+ options: [
62
+ { value: 'domestic', text: 'Domestic' },
63
+ { value: 'international', text: 'International' }
64
+ ]
65
+ },
66
+ // Section 3: Visa (section-wide condition based on tripType)
67
+ visaRequired: {
68
+ type: COMPONENT_TYPES.BOOLEAN,
69
+ title: 'Visa Required',
70
+ question: 'Do you need a visa for your destination?',
71
+ fieldName: 'visaRequired',
72
+ url: 'visa-required',
73
+ label: 'Visa required?'
74
+ },
75
+ visaType: {
76
+ type: COMPONENT_TYPES.RADIO,
77
+ title: 'Visa Type',
78
+ question: 'What type of visa do you need?',
79
+ fieldName: 'visaType',
80
+ url: 'visa-type',
81
+ label: 'Select a visa type',
82
+ options: [
83
+ { value: 'tourist', text: 'Tourist' },
84
+ { value: 'business', text: 'Business' }
85
+ ]
86
+ },
87
+ // Section 4: Accommodation (multi-question condition)
88
+ accommodation: {
89
+ type: COMPONENT_TYPES.RADIO,
90
+ title: 'Accommodation',
91
+ question: 'Where will you stay?',
92
+ fieldName: 'accommodation',
93
+ url: 'accommodation',
94
+ label: 'Select accommodation type',
95
+ options: [
96
+ { value: 'hotel', text: 'Hotel' },
97
+ { value: 'hostel', text: 'Hostel' },
98
+ { value: 'apartment', text: 'Apartment' }
99
+ ]
100
+ },
101
+ hotelStars: {
102
+ type: COMPONENT_TYPES.RADIO,
103
+ title: 'Hotel Stars',
104
+ question: 'How many stars for your hotel?',
105
+ fieldName: 'hotelStars',
106
+ url: 'hotel-stars',
107
+ label: 'Select star rating',
108
+ options: [
109
+ { value: '3', text: '3 stars' },
110
+ { value: '4', text: '4 stars' },
111
+ { value: '5', text: '5 stars' }
112
+ ]
113
+ },
114
+ hotelBreakfast: {
115
+ type: COMPONENT_TYPES.BOOLEAN,
116
+ title: 'Hotel Breakfast',
117
+ question: 'Do you want breakfast included?',
118
+ fieldName: 'hotelBreakfast',
119
+ url: 'hotel-breakfast',
120
+ label: 'Include breakfast?'
121
+ },
122
+ // Section 5: Contact (always shown)
123
+ contactEmail: {
124
+ type: COMPONENT_TYPES.EMAIL,
125
+ title: 'Contact Email',
126
+ question: 'What is your email address?',
127
+ fieldName: 'contactEmail',
128
+ url: 'contact-email',
129
+ label: 'Email address'
130
+ }
131
+ };
132
+
133
+ /**
134
+ * Create the questions for conditional tests
135
+ */
136
+ const conditionalQuestions = createQuestions(conditionalQuestionProps, questionClasses, {});
137
+
138
+ /**
139
+ * Create a single journey with various conditional logic scenarios:
140
+ *
141
+ * Section 1 - Insurance (simple + chained conditions):
142
+ * - wantsInsurance: always shown
143
+ * - insuranceLevel: shown if wantsInsurance = 'yes' (simple condition)
144
+ * - premiumBenefits: shown if insuranceLevel = 'premium' (chained condition)
145
+ *
146
+ * Section 2 - Trip:
147
+ * - tripType: always shown
148
+ *
149
+ * Section 3 - Visa (section-wide condition):
150
+ * - Entire section shown only if tripType = 'international'
151
+ * - visaRequired: shown if section condition met
152
+ * - visaType: shown if section condition met AND visaRequired = 'yes'
153
+ *
154
+ * Section 4 - Accommodation (multi-question condition):
155
+ * - accommodation: always shown
156
+ * - hotelStars + hotelBreakfast: shown only if accommodation = 'hotel' (multi-question group)
157
+ *
158
+ * Section 5 - Contact:
159
+ * - contactEmail: always shown
160
+ */
161
+ function createConditionalJourney(questions, response) {
162
+ return new Journey({
163
+ journeyId: JOURNEY_ID,
164
+ sections: [
165
+ // Section 1: Insurance with simple and chained conditions
166
+ new Section('Insurance', 'insurance')
167
+ .addQuestion(questions.wantsInsurance)
168
+ .addQuestion(questions.insuranceLevel)
169
+ .withCondition(whenQuestionHasAnswer(questions.wantsInsurance, BOOLEAN_OPTIONS.YES))
170
+ .addQuestion(questions.premiumBenefits)
171
+ .withCondition(whenQuestionHasAnswer(questions.insuranceLevel, 'premium')),
172
+
173
+ // Section 2: Trip type (determines section condition for visa)
174
+ new Section('Trip Details', 'trip').addQuestion(questions.tripType),
175
+
176
+ // Section 3: Visa with section-wide condition
177
+ new Section('Visa', 'visa')
178
+ .withSectionCondition(whenQuestionHasAnswer(questions.tripType, 'international'))
179
+ .addQuestion(questions.visaRequired)
180
+ .addQuestion(questions.visaType)
181
+ .withCondition(whenQuestionHasAnswer(questions.visaRequired, BOOLEAN_OPTIONS.YES)),
182
+
183
+ // Section 4: Accommodation with multi-question condition
184
+ new Section('Accommodation', 'accommodation')
185
+ .addQuestion(questions.accommodation)
186
+ .startMultiQuestionCondition('hotel-group', whenQuestionHasAnswer(questions.accommodation, 'hotel'))
187
+ .addQuestion(questions.hotelStars)
188
+ .addQuestion(questions.hotelBreakfast)
189
+ .endMultiQuestionCondition('hotel-group'),
190
+
191
+ // Section 5: Contact (always shown)
192
+ new Section('Contact', 'contact').addQuestion(questions.contactEmail)
193
+ ],
194
+ taskListUrl: 'check-your-answers',
195
+ journeyTemplate: 'views/layout-journey.njk',
196
+ taskListTemplate: 'views/layout-check-your-answers.njk',
197
+ journeyTitle: 'Conditional Journey',
198
+ returnToListing: false,
199
+ makeBaseUrl: () => '/',
200
+ initialBackLink: '/start',
201
+ response
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Helper to create an app with the conditional journey
207
+ * @param {import('node:test').TestContext} ctx
208
+ * @returns {Promise<import('#test/utils/test-server.js').TestServer>}
209
+ */
210
+ function createAppWithJourney(ctx) {
211
+ return createAppWithQuestions(ctx, {
212
+ journeyId: JOURNEY_ID,
213
+ questions: conditionalQuestions,
214
+ createJourneyFn: createConditionalJourney
215
+ });
216
+ }
217
+
218
+ const q = conditionalQuestionProps;
219
+
220
+ describe('conditional journey tests', () => {
221
+ describe('simple condition (whenQuestionHasAnswer on single question)', () => {
222
+ it('should show conditional question when condition is met', async (ctx) => {
223
+ const testServer = await createAppWithJourney(ctx);
224
+
225
+ // First, render the initial question
226
+ await renderQuestionCheck(ctx, testServer, '/insurance/wants-insurance', q.wantsInsurance.question);
227
+
228
+ // Answer 'yes' to wantsInsurance
229
+ const location = await postAnswer(testServer, '/insurance/wants-insurance', {
230
+ [q.wantsInsurance.fieldName]: BOOLEAN_OPTIONS.YES
231
+ });
232
+
233
+ // Should redirect to insuranceLevel (conditional question)
234
+ assert.strictEqual(location, 'insurance/insurance-level');
235
+
236
+ // Render the conditional question
237
+ await renderQuestionCheck(ctx, testServer, '/insurance/insurance-level', q.insuranceLevel.question);
238
+ });
239
+
240
+ it('should skip conditional question when condition is not met', async (ctx) => {
241
+ const testServer = await createAppWithJourney(ctx);
242
+
243
+ // Answer 'no' to wantsInsurance
244
+ const location = await postAnswer(testServer, '/insurance/wants-insurance', {
245
+ [q.wantsInsurance.fieldName]: BOOLEAN_OPTIONS.NO
246
+ });
247
+
248
+ // Should skip insuranceLevel and premiumBenefits, go to next section (trip)
249
+ assert.strictEqual(location, 'trip/trip-type');
250
+ });
251
+ });
252
+
253
+ describe('chained conditions (multiple dependent conditions)', () => {
254
+ it('should show both conditional questions when all conditions are met', async (ctx) => {
255
+ const testServer = await createAppWithJourney(ctx);
256
+
257
+ // Answer 'yes' to wantsInsurance
258
+ await postAnswer(testServer, '/insurance/wants-insurance', {
259
+ [q.wantsInsurance.fieldName]: BOOLEAN_OPTIONS.YES
260
+ });
261
+
262
+ // Answer 'premium' to insuranceLevel
263
+ const location = await postAnswer(testServer, '/insurance/insurance-level', {
264
+ [q.insuranceLevel.fieldName]: 'premium'
265
+ });
266
+
267
+ // Should go to premiumBenefits
268
+ assert.strictEqual(location, 'insurance/premium-benefits');
269
+
270
+ // Render the premium benefits question
271
+ await renderQuestionCheck(ctx, testServer, '/insurance/premium-benefits', q.premiumBenefits.question);
272
+ });
273
+
274
+ it('should skip second conditional question when its condition is not met', async (ctx) => {
275
+ const testServer = await createAppWithJourney(ctx);
276
+
277
+ // Answer 'yes' to wantsInsurance
278
+ await postAnswer(testServer, '/insurance/wants-insurance', {
279
+ [q.wantsInsurance.fieldName]: BOOLEAN_OPTIONS.YES
280
+ });
281
+
282
+ // Answer 'basic' to insuranceLevel (not 'premium')
283
+ const location = await postAnswer(testServer, '/insurance/insurance-level', {
284
+ [q.insuranceLevel.fieldName]: 'basic'
285
+ });
286
+
287
+ // Should skip premiumBenefits and go to next section (trip)
288
+ assert.strictEqual(location, 'trip/trip-type');
289
+ });
290
+
291
+ it('should skip all conditional questions when first condition is not met', async (ctx) => {
292
+ const testServer = await createAppWithJourney(ctx);
293
+
294
+ // Answer 'no' to wantsInsurance
295
+ const location = await postAnswer(testServer, '/insurance/wants-insurance', {
296
+ [q.wantsInsurance.fieldName]: BOOLEAN_OPTIONS.NO
297
+ });
298
+
299
+ // Should skip insuranceLevel and premiumBenefits, go to next section (trip)
300
+ assert.strictEqual(location, 'trip/trip-type');
301
+ });
302
+ });
303
+
304
+ describe('section-wide conditions (withSectionCondition)', () => {
305
+ it('should show section questions when section condition is met', async (ctx) => {
306
+ const testServer = await createAppWithJourney(ctx);
307
+
308
+ // Answer 'international' to tripType
309
+ const location = await postAnswer(testServer, '/trip/trip-type', {
310
+ [q.tripType.fieldName]: 'international'
311
+ });
312
+
313
+ // Should go to visa section (visaRequired)
314
+ assert.strictEqual(location, 'visa/visa-required');
315
+
316
+ // Render the visa question
317
+ await renderQuestionCheck(ctx, testServer, '/visa/visa-required', q.visaRequired.question);
318
+ });
319
+
320
+ it('should skip entire section when section condition is not met', async (ctx) => {
321
+ const testServer = await createAppWithJourney(ctx);
322
+
323
+ // Answer 'domestic' to tripType
324
+ const location = await postAnswer(testServer, '/trip/trip-type', {
325
+ [q.tripType.fieldName]: 'domestic'
326
+ });
327
+
328
+ // Should skip visa section and go to accommodation section
329
+ assert.strictEqual(location, 'accommodation/accommodation');
330
+ });
331
+
332
+ it('should combine section condition with question condition', async (ctx) => {
333
+ const testServer = await createAppWithJourney(ctx);
334
+
335
+ // Answer 'international' to tripType
336
+ await postAnswer(testServer, '/trip/trip-type', {
337
+ [q.tripType.fieldName]: 'international'
338
+ });
339
+
340
+ // Answer 'yes' to visaRequired
341
+ const location = await postAnswer(testServer, '/visa/visa-required', {
342
+ [q.visaRequired.fieldName]: BOOLEAN_OPTIONS.YES
343
+ });
344
+
345
+ // Should go to visaType (condition met)
346
+ assert.strictEqual(location, 'visa/visa-type');
347
+
348
+ // Render the visa type question
349
+ await renderQuestionCheck(ctx, testServer, '/visa/visa-type', q.visaType.question);
350
+ });
351
+
352
+ it('should skip conditional question within section when its condition is not met', async (ctx) => {
353
+ const testServer = await createAppWithJourney(ctx);
354
+
355
+ // Answer 'international' to tripType
356
+ await postAnswer(testServer, '/trip/trip-type', {
357
+ [q.tripType.fieldName]: 'international'
358
+ });
359
+
360
+ // Answer 'no' to visaRequired
361
+ const location = await postAnswer(testServer, '/visa/visa-required', {
362
+ [q.visaRequired.fieldName]: BOOLEAN_OPTIONS.NO
363
+ });
364
+
365
+ // Should skip visaType and go to accommodation section
366
+ assert.strictEqual(location, 'accommodation/accommodation');
367
+ });
368
+ });
369
+
370
+ describe('multi-question conditions (startMultiQuestionCondition)', () => {
371
+ it('should show all questions in group when condition is met', async (ctx) => {
372
+ const testServer = await createAppWithJourney(ctx);
373
+
374
+ // Answer 'hotel' to accommodation
375
+ const location = await postAnswer(testServer, '/accommodation/accommodation', {
376
+ [q.accommodation.fieldName]: 'hotel'
377
+ });
378
+
379
+ // Should go to hotelStars (first question in group)
380
+ assert.strictEqual(location, 'accommodation/hotel-stars');
381
+
382
+ // Render the hotel stars question
383
+ await renderQuestionCheck(ctx, testServer, '/accommodation/hotel-stars', q.hotelStars.question);
384
+
385
+ // Answer hotelStars
386
+ const nextLocation = await postAnswer(testServer, '/accommodation/hotel-stars', {
387
+ [q.hotelStars.fieldName]: '4'
388
+ });
389
+
390
+ // Should go to hotelBreakfast (second question in group)
391
+ assert.strictEqual(nextLocation, 'accommodation/hotel-breakfast');
392
+
393
+ // Render the hotel breakfast question
394
+ await renderQuestionCheck(ctx, testServer, '/accommodation/hotel-breakfast', q.hotelBreakfast.question);
395
+ });
396
+
397
+ it('should skip all questions in group when condition is not met', async (ctx) => {
398
+ const testServer = await createAppWithJourney(ctx);
399
+
400
+ // Answer 'hostel' to accommodation (not 'hotel')
401
+ const location = await postAnswer(testServer, '/accommodation/accommodation', {
402
+ [q.accommodation.fieldName]: 'hostel'
403
+ });
404
+
405
+ // Should skip hotelStars and hotelBreakfast, go to contact section
406
+ assert.strictEqual(location, 'contact/contact-email');
407
+ });
408
+
409
+ it('should proceed to question after group ends', async (ctx) => {
410
+ const testServer = await createAppWithJourney(ctx);
411
+
412
+ // Answer 'hotel' to accommodation
413
+ await postAnswer(testServer, '/accommodation/accommodation', {
414
+ [q.accommodation.fieldName]: 'hotel'
415
+ });
416
+
417
+ // Answer hotelStars
418
+ await postAnswer(testServer, '/accommodation/hotel-stars', {
419
+ [q.hotelStars.fieldName]: '5'
420
+ });
421
+
422
+ // Answer hotelBreakfast (last in group)
423
+ const location = await postAnswer(testServer, '/accommodation/hotel-breakfast', {
424
+ [q.hotelBreakfast.fieldName]: BOOLEAN_OPTIONS.YES
425
+ });
426
+
427
+ // Should go to contact section (after group ends)
428
+ assert.strictEqual(location, 'contact/contact-email');
429
+ });
430
+ });
431
+ });