@defra/forms-engine-plugin 0.0.2 → 0.0.4

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 (330) hide show
  1. package/.public/assets/fonts/bold-affa96571d-v2.woff +0 -0
  2. package/.public/assets/fonts/bold-b542beb274-v2.woff2 +0 -0
  3. package/.public/assets/fonts/light-94a07e06a1-v2.woff2 +0 -0
  4. package/.public/assets/fonts/light-f591b13f7d-v2.woff +0 -0
  5. package/.public/assets/images/favicon.ico +0 -0
  6. package/.public/assets/images/favicon.svg +1 -0
  7. package/.public/assets/images/govuk-crest.svg +1 -0
  8. package/.public/assets/images/govuk-icon-180.png +0 -0
  9. package/.public/assets/images/govuk-icon-192.png +0 -0
  10. package/.public/assets/images/govuk-icon-512.png +0 -0
  11. package/.public/assets/images/govuk-icon-mask.svg +1 -0
  12. package/.public/assets/images/govuk-opengraph-image.png +0 -0
  13. package/.public/assets/manifest.json +39 -0
  14. package/.public/assets-manifest.json +24 -0
  15. package/.public/javascripts/application.0fd8c18.min.js +3 -0
  16. package/.public/javascripts/application.0fd8c18.min.js.LICENSE.txt +58 -0
  17. package/.public/javascripts/application.0fd8c18.min.js.map +1 -0
  18. package/.public/javascripts/file-upload.b2f18f5.min.js +2 -0
  19. package/.public/javascripts/file-upload.b2f18f5.min.js.map +1 -0
  20. package/.public/javascripts/vendor/accessible-autocomplete.275d332.min.js +2 -0
  21. package/.public/javascripts/vendor/accessible-autocomplete.275d332.min.js.map +1 -0
  22. package/.public/stylesheets/application.e340021.min.css +14 -0
  23. package/.public/stylesheets/application.e340021.min.css.map +1 -0
  24. package/.server/server/plugins/nunjucks/environment.js +3 -3
  25. package/.server/server/plugins/nunjucks/environment.js.map +1 -1
  26. package/package.json +6 -2
  27. package/.browserslistrc +0 -16
  28. package/.editorconfig +0 -9
  29. package/.eslintrc.cjs +0 -266
  30. package/.github/dependabot.yml +0 -85
  31. package/.github/workflows/check-pull-request.yml +0 -209
  32. package/.github/workflows/pr-notifier.yml +0 -98
  33. package/.github/workflows/publish.yml +0 -111
  34. package/.husky/pre-commit +0 -1
  35. package/.lintstagedrc.js +0 -4
  36. package/.nvmrc +0 -1
  37. package/.prettierignore +0 -8
  38. package/.prettierrc.cjs +0 -26
  39. package/Dockerfile +0 -61
  40. package/Procfile +0 -1
  41. package/babel.config.cjs +0 -55
  42. package/compose/aws.env +0 -4
  43. package/compose/start-localstack.sh +0 -26
  44. package/docker-compose.yaml +0 -86
  45. package/globals.d.ts +0 -1
  46. package/jest.config.cjs +0 -54
  47. package/jest.environment.js +0 -4
  48. package/jest.setup.cjs +0 -14
  49. package/postcss.config.js +0 -26
  50. package/sonar-project.properties +0 -17
  51. package/src/client/javascripts/application.js +0 -87
  52. package/src/client/javascripts/file-upload.js +0 -386
  53. package/src/client/stylesheets/_code.scss +0 -33
  54. package/src/client/stylesheets/_govuk-frontend.scss +0 -4
  55. package/src/client/stylesheets/_prose.scss +0 -56
  56. package/src/client/stylesheets/_service-banner.scss +0 -24
  57. package/src/client/stylesheets/_summary-list.scss +0 -28
  58. package/src/client/stylesheets/_tag-env.scss +0 -24
  59. package/src/client/stylesheets/application.scss +0 -14
  60. package/src/common/cookies.js +0 -58
  61. package/src/common/cookies.test.js +0 -23
  62. package/src/common/types.js +0 -5
  63. package/src/config/index.ts +0 -271
  64. package/src/index.ts +0 -31
  65. package/src/server/common/helpers/logging/logger-options.test.ts +0 -50
  66. package/src/server/common/helpers/logging/logger-options.ts +0 -46
  67. package/src/server/common/helpers/logging/logger.ts +0 -7
  68. package/src/server/common/helpers/logging/request-logger.ts +0 -9
  69. package/src/server/common/helpers/logging/request-tracing.js +0 -10
  70. package/src/server/common/helpers/redis-client.js +0 -70
  71. package/src/server/constants.js +0 -1
  72. package/src/server/forms/README.md +0 -10
  73. package/src/server/forms/components.json +0 -1015
  74. package/src/server/forms/report-a-terrorist.json +0 -270
  75. package/src/server/forms/runner-components-test.json +0 -365
  76. package/src/server/forms/test.json +0 -581
  77. package/src/server/index.test.ts +0 -582
  78. package/src/server/index.ts +0 -140
  79. package/src/server/plugins/blankie.test.ts +0 -73
  80. package/src/server/plugins/blankie.ts +0 -48
  81. package/src/server/plugins/crumb.ts +0 -20
  82. package/src/server/plugins/engine/README.md +0 -87
  83. package/src/server/plugins/engine/components/AutocompleteField.test.ts +0 -294
  84. package/src/server/plugins/engine/components/AutocompleteField.ts +0 -49
  85. package/src/server/plugins/engine/components/CheckboxesField.test.ts +0 -379
  86. package/src/server/plugins/engine/components/CheckboxesField.ts +0 -106
  87. package/src/server/plugins/engine/components/ComponentBase.ts +0 -97
  88. package/src/server/plugins/engine/components/ComponentCollection.ts +0 -278
  89. package/src/server/plugins/engine/components/DatePartsField.test.ts +0 -822
  90. package/src/server/plugins/engine/components/DatePartsField.ts +0 -264
  91. package/src/server/plugins/engine/components/Details.test.ts +0 -49
  92. package/src/server/plugins/engine/components/Details.ts +0 -30
  93. package/src/server/plugins/engine/components/EmailAddressField.test.ts +0 -395
  94. package/src/server/plugins/engine/components/EmailAddressField.ts +0 -55
  95. package/src/server/plugins/engine/components/FileUploadField.test.ts +0 -778
  96. package/src/server/plugins/engine/components/FileUploadField.ts +0 -262
  97. package/src/server/plugins/engine/components/FormComponent.ts +0 -249
  98. package/src/server/plugins/engine/components/Html.test.ts +0 -48
  99. package/src/server/plugins/engine/components/Html.ts +0 -29
  100. package/src/server/plugins/engine/components/InsetText.test.ts +0 -48
  101. package/src/server/plugins/engine/components/InsetText.ts +0 -27
  102. package/src/server/plugins/engine/components/List.test.ts +0 -76
  103. package/src/server/plugins/engine/components/List.ts +0 -72
  104. package/src/server/plugins/engine/components/ListFormComponent.ts +0 -140
  105. package/src/server/plugins/engine/components/MonthYearField.test.ts +0 -567
  106. package/src/server/plugins/engine/components/MonthYearField.ts +0 -222
  107. package/src/server/plugins/engine/components/MultilineTextField.test.ts +0 -558
  108. package/src/server/plugins/engine/components/MultilineTextField.ts +0 -138
  109. package/src/server/plugins/engine/components/NumberField.test.ts +0 -701
  110. package/src/server/plugins/engine/components/NumberField.ts +0 -163
  111. package/src/server/plugins/engine/components/RadiosField.test.ts +0 -288
  112. package/src/server/plugins/engine/components/RadiosField.ts +0 -24
  113. package/src/server/plugins/engine/components/SelectField.test.ts +0 -288
  114. package/src/server/plugins/engine/components/SelectField.ts +0 -47
  115. package/src/server/plugins/engine/components/SelectionControlField.ts +0 -43
  116. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +0 -356
  117. package/src/server/plugins/engine/components/TelephoneNumberField.ts +0 -67
  118. package/src/server/plugins/engine/components/TextField.test.ts +0 -489
  119. package/src/server/plugins/engine/components/TextField.ts +0 -96
  120. package/src/server/plugins/engine/components/UkAddressField.test.ts +0 -623
  121. package/src/server/plugins/engine/components/UkAddressField.ts +0 -172
  122. package/src/server/plugins/engine/components/YesNoField.test.ts +0 -248
  123. package/src/server/plugins/engine/components/YesNoField.ts +0 -31
  124. package/src/server/plugins/engine/components/constants.ts +0 -1
  125. package/src/server/plugins/engine/components/helpers.ts +0 -330
  126. package/src/server/plugins/engine/components/index.ts +0 -24
  127. package/src/server/plugins/engine/components/types.ts +0 -117
  128. package/src/server/plugins/engine/configureEnginePlugin.ts +0 -47
  129. package/src/server/plugins/engine/helpers.test.ts +0 -791
  130. package/src/server/plugins/engine/helpers.ts +0 -379
  131. package/src/server/plugins/engine/index.ts +0 -7
  132. package/src/server/plugins/engine/models/FormModel.test.ts +0 -42
  133. package/src/server/plugins/engine/models/FormModel.ts +0 -443
  134. package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
  135. package/src/server/plugins/engine/models/Section.ts +0 -0
  136. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +0 -209
  137. package/src/server/plugins/engine/models/SummaryViewModel.ts +0 -220
  138. package/src/server/plugins/engine/models/index.ts +0 -2
  139. package/src/server/plugins/engine/models/types.ts +0 -114
  140. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +0 -143
  141. package/src/server/plugins/engine/outputFormatters/human/v1.ts +0 -73
  142. package/src/server/plugins/engine/outputFormatters/index.test.ts +0 -17
  143. package/src/server/plugins/engine/outputFormatters/index.ts +0 -44
  144. package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +0 -229
  145. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +0 -140
  146. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +0 -229
  147. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +0 -153
  148. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +0 -1108
  149. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +0 -446
  150. package/src/server/plugins/engine/pageControllers/PageController.test.ts +0 -205
  151. package/src/server/plugins/engine/pageControllers/PageController.ts +0 -176
  152. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +0 -1264
  153. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +0 -561
  154. package/src/server/plugins/engine/pageControllers/README.md +0 -28
  155. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +0 -264
  156. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +0 -458
  157. package/src/server/plugins/engine/pageControllers/StartPageController.ts +0 -18
  158. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +0 -50
  159. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +0 -261
  160. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +0 -28
  161. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +0 -19
  162. package/src/server/plugins/engine/pageControllers/helpers.test.ts +0 -198
  163. package/src/server/plugins/engine/pageControllers/helpers.ts +0 -101
  164. package/src/server/plugins/engine/pageControllers/index.ts +0 -10
  165. package/src/server/plugins/engine/pageControllers/validationOptions.ts +0 -89
  166. package/src/server/plugins/engine/plugin.ts +0 -673
  167. package/src/server/plugins/engine/services/formSubmissionService.js +0 -46
  168. package/src/server/plugins/engine/services/formsService.js +0 -46
  169. package/src/server/plugins/engine/services/formsService.test.js +0 -90
  170. package/src/server/plugins/engine/services/index.js +0 -3
  171. package/src/server/plugins/engine/services/notifyService.test.ts +0 -132
  172. package/src/server/plugins/engine/services/notifyService.ts +0 -64
  173. package/src/server/plugins/engine/services/uploadService.js +0 -60
  174. package/src/server/plugins/engine/types.ts +0 -315
  175. package/src/server/plugins/engine/views/components/autocompletefield.html +0 -5
  176. package/src/server/plugins/engine/views/components/checkboxesfield.html +0 -5
  177. package/src/server/plugins/engine/views/components/datepartsfield.html +0 -5
  178. package/src/server/plugins/engine/views/components/details.html +0 -6
  179. package/src/server/plugins/engine/views/components/emailaddressfield.html +0 -5
  180. package/src/server/plugins/engine/views/components/fileuploadfield-key.html +0 -8
  181. package/src/server/plugins/engine/views/components/fileuploadfield-value.html +0 -3
  182. package/src/server/plugins/engine/views/components/fileuploadfield.html +0 -24
  183. package/src/server/plugins/engine/views/components/html.html +0 -3
  184. package/src/server/plugins/engine/views/components/insettext.html +0 -7
  185. package/src/server/plugins/engine/views/components/list.html +0 -36
  186. package/src/server/plugins/engine/views/components/monthyearfield.html +0 -5
  187. package/src/server/plugins/engine/views/components/multilinetextfield.html +0 -10
  188. package/src/server/plugins/engine/views/components/numberfield.html +0 -5
  189. package/src/server/plugins/engine/views/components/radiosfield.html +0 -5
  190. package/src/server/plugins/engine/views/components/selectfield.html +0 -5
  191. package/src/server/plugins/engine/views/components/telephonenumberfield.html +0 -5
  192. package/src/server/plugins/engine/views/components/textfield.html +0 -5
  193. package/src/server/plugins/engine/views/components/ukaddressfield.html +0 -25
  194. package/src/server/plugins/engine/views/components/yesnofield.html +0 -5
  195. package/src/server/plugins/engine/views/file-upload.html +0 -45
  196. package/src/server/plugins/engine/views/index.html +0 -39
  197. package/src/server/plugins/engine/views/item-delete.html +0 -56
  198. package/src/server/plugins/engine/views/partials/components.html +0 -6
  199. package/src/server/plugins/engine/views/partials/conditional-components.html +0 -3
  200. package/src/server/plugins/engine/views/partials/debug.html +0 -44
  201. package/src/server/plugins/engine/views/partials/form.html +0 -15
  202. package/src/server/plugins/engine/views/partials/heading.html +0 -16
  203. package/src/server/plugins/engine/views/partials/preview-banner.html +0 -32
  204. package/src/server/plugins/engine/views/partials/preview-banner.test.js +0 -122
  205. package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +0 -10
  206. package/src/server/plugins/engine/views/repeat-list-summary.html +0 -53
  207. package/src/server/plugins/errorPages.ts +0 -58
  208. package/src/server/plugins/nunjucks/context.js +0 -88
  209. package/src/server/plugins/nunjucks/context.test.js +0 -142
  210. package/src/server/plugins/nunjucks/enviroment.test.js +0 -201
  211. package/src/server/plugins/nunjucks/environment.js +0 -116
  212. package/src/server/plugins/nunjucks/filters/answer.js +0 -27
  213. package/src/server/plugins/nunjucks/filters/answer.test.js +0 -89
  214. package/src/server/plugins/nunjucks/filters/evaluate.js +0 -21
  215. package/src/server/plugins/nunjucks/filters/field.js +0 -28
  216. package/src/server/plugins/nunjucks/filters/field.test.js +0 -75
  217. package/src/server/plugins/nunjucks/filters/highlight.js +0 -11
  218. package/src/server/plugins/nunjucks/filters/href.js +0 -30
  219. package/src/server/plugins/nunjucks/filters/href.test.js +0 -80
  220. package/src/server/plugins/nunjucks/filters/index.js +0 -8
  221. package/src/server/plugins/nunjucks/filters/inspect.js +0 -15
  222. package/src/server/plugins/nunjucks/filters/page.js +0 -24
  223. package/src/server/plugins/nunjucks/filters/page.test.js +0 -65
  224. package/src/server/plugins/nunjucks/index.js +0 -3
  225. package/src/server/plugins/nunjucks/plugin.js +0 -40
  226. package/src/server/plugins/nunjucks/render.js +0 -42
  227. package/src/server/plugins/nunjucks/types.js +0 -40
  228. package/src/server/plugins/pulse.ts +0 -11
  229. package/src/server/plugins/router.ts +0 -201
  230. package/src/server/plugins/session.ts +0 -28
  231. package/src/server/routes/health.js +0 -13
  232. package/src/server/routes/health.test.js +0 -35
  233. package/src/server/routes/index.test.ts +0 -125
  234. package/src/server/routes/index.ts +0 -2
  235. package/src/server/routes/public.ts +0 -47
  236. package/src/server/routes/types.ts +0 -48
  237. package/src/server/schemas/index.ts +0 -34
  238. package/src/server/secure-context.js +0 -43
  239. package/src/server/services/cacheService.test.ts +0 -276
  240. package/src/server/services/cacheService.ts +0 -131
  241. package/src/server/services/httpService.test.js +0 -491
  242. package/src/server/services/httpService.ts +0 -50
  243. package/src/server/services/index.ts +0 -1
  244. package/src/server/types.ts +0 -54
  245. package/src/server/utils/notify.test.ts +0 -37
  246. package/src/server/utils/notify.ts +0 -50
  247. package/src/server/utils/secure-context/get-trust-store-certs.js +0 -11
  248. package/src/server/utils/secure-context/get-trust-store-certs.test.js +0 -19
  249. package/src/server/utils/utils.js +0 -24
  250. package/src/server/utils/utils.test.js +0 -54
  251. package/src/server/views/404.html +0 -16
  252. package/src/server/views/500.html +0 -19
  253. package/src/server/views/components/debug/macro.njk +0 -3
  254. package/src/server/views/components/debug/template.njk +0 -13
  255. package/src/server/views/components/service-banner/macro.njk +0 -3
  256. package/src/server/views/components/service-banner/template.njk +0 -20
  257. package/src/server/views/components/service-banner/template.test.js +0 -43
  258. package/src/server/views/components/tag-env/macro.njk +0 -3
  259. package/src/server/views/components/tag-env/template.njk +0 -30
  260. package/src/server/views/components/tag-env/template.test.js +0 -66
  261. package/src/server/views/confirmation.html +0 -19
  262. package/src/server/views/help/accessibility-statement.html +0 -58
  263. package/src/server/views/help/cookie-preferences.html +0 -57
  264. package/src/server/views/help/cookies.html +0 -71
  265. package/src/server/views/help/get-support.html +0 -37
  266. package/src/server/views/help/privacy-notice.html +0 -68
  267. package/src/server/views/help/terms-and-conditions.html +0 -83
  268. package/src/server/views/layout.html +0 -199
  269. package/src/server/views/summary.html +0 -50
  270. package/src/typings/hapi/index.d.ts +0 -95
  271. package/src/typings/hapi-tracing/index.d.ts +0 -6
  272. package/src/typings/index.d.ts +0 -3
  273. package/src/typings/joi/index.d.ts +0 -22
  274. package/stylelint.config.js +0 -10
  275. package/test/client/javascripts/file-upload.test.js +0 -1197
  276. package/test/condition/checkboxes.test.js +0 -112
  277. package/test/condition/radios.test.js +0 -112
  278. package/test/condition/text.test.js +0 -103
  279. package/test/fixtures/assets-manifest.json +0 -4
  280. package/test/fixtures/form.js +0 -86
  281. package/test/fixtures/index.js +0 -2
  282. package/test/fixtures/list.js +0 -92
  283. package/test/form/cookies.test.js +0 -338
  284. package/test/form/csrf.test.js +0 -87
  285. package/test/form/definitions/basic.js +0 -101
  286. package/test/form/definitions/blank.js +0 -10
  287. package/test/form/definitions/checkboxes.json +0 -88
  288. package/test/form/definitions/components.json +0 -452
  289. package/test/form/definitions/conditional-reveal.js +0 -140
  290. package/test/form/definitions/conditions-basic.js +0 -187
  291. package/test/form/definitions/conditions-complex.js +0 -338
  292. package/test/form/definitions/conditions-dates.js +0 -78
  293. package/test/form/definitions/conditions-escaping.js +0 -143
  294. package/test/form/definitions/demo-cph-number.js +0 -3099
  295. package/test/form/definitions/feedback.json +0 -45
  296. package/test/form/definitions/fields-optional.js +0 -402
  297. package/test/form/definitions/fields-required.js +0 -402
  298. package/test/form/definitions/file-upload-basic.js +0 -44
  299. package/test/form/definitions/file-upload.js +0 -66
  300. package/test/form/definitions/minimal.js +0 -39
  301. package/test/form/definitions/phase-alpha.json +0 -33
  302. package/test/form/definitions/phase-default.json +0 -26
  303. package/test/form/definitions/radios.json +0 -88
  304. package/test/form/definitions/repeat-mixed.js +0 -54
  305. package/test/form/definitions/repeat.js +0 -70
  306. package/test/form/definitions/status.json +0 -126
  307. package/test/form/definitions/templates.js +0 -183
  308. package/test/form/definitions/test.json +0 -581
  309. package/test/form/definitions/text.json +0 -75
  310. package/test/form/definitions/titles.json +0 -170
  311. package/test/form/definitions.test.js +0 -47
  312. package/test/form/exit-page.test.js +0 -210
  313. package/test/form/feedback.test.js +0 -68
  314. package/test/form/fields-optional.test.js +0 -237
  315. package/test/form/fields-required.test.js +0 -294
  316. package/test/form/file-upload.test.js +0 -313
  317. package/test/form/govuk-notify.test.js +0 -449
  318. package/test/form/journey-basic.test.js +0 -444
  319. package/test/form/persist-files.test.js +0 -227
  320. package/test/form/phase-banner.test.js +0 -71
  321. package/test/form/repeat.test.js +0 -628
  322. package/test/form/summary-submission-email.test.js +0 -95
  323. package/test/form/template.test.js +0 -288
  324. package/test/form/titles.test.js +0 -204
  325. package/test/helpers/component-helpers.js +0 -74
  326. package/test/utils/get-cookie.js +0 -42
  327. package/test/utils/get-form-definitions.js +0 -18
  328. package/tmp.pdf +0 -1
  329. package/tsconfig.json +0 -28
  330. package/webpack.config.js +0 -208
@@ -1,1264 +0,0 @@
1
- import { type PageQuestion } from '@defra/forms-model'
2
- import { type ResponseToolkit } from '@hapi/hapi'
3
-
4
- import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
5
- import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
6
- import {
7
- type FormContext,
8
- type FormContextRequest,
9
- type FormPageViewModel,
10
- type FormState,
11
- type FormSubmissionState
12
- } from '~/src/server/plugins/engine/types.js'
13
- import { type FormRequest } from '~/src/server/routes/types.js'
14
- import { CacheService } from '~/src/server/services/cacheService.js'
15
- import conditionalReveal from '~/test/form/definitions/conditional-reveal.js'
16
- import definitionConditionsBasic, {
17
- V2 as definitionConditionsBasicV2
18
- } from '~/test/form/definitions/conditions-basic.js'
19
- import definitionConditionsComplex from '~/test/form/definitions/conditions-complex.js'
20
- import definitionConditionsDates from '~/test/form/definitions/conditions-dates.js'
21
-
22
- describe('QuestionPageController', () => {
23
- let page1: PageQuestion
24
- let page1Url: URL
25
-
26
- let page2: PageQuestion
27
- let page2Url: URL
28
-
29
- let model: FormModel
30
- let controller1: QuestionPageController
31
- let controller2: QuestionPageController
32
- let requestPage1: FormRequest
33
- let requestPage2: FormRequest
34
-
35
- beforeEach(() => {
36
- const { pages } = definitionConditionsBasic
37
-
38
- page1 = pages[0]
39
- page1Url = new URL('http://example.com/test/first-page')
40
-
41
- page2 = pages[1]
42
- page2Url = new URL('http://example.com/test/second-page')
43
-
44
- model = new FormModel(definitionConditionsBasic, {
45
- basePath: 'test'
46
- })
47
-
48
- controller1 = new QuestionPageController(model, page1)
49
- controller2 = new QuestionPageController(model, page2)
50
-
51
- requestPage1 = {
52
- method: 'get',
53
- url: page1Url,
54
- path: page1Url.pathname,
55
- params: {
56
- path: 'first-page',
57
- slug: 'test'
58
- },
59
- query: {},
60
- app: { model }
61
- } as FormRequest
62
-
63
- requestPage2 = {
64
- method: 'get',
65
- url: page2Url,
66
- path: page2Url.pathname,
67
- params: {
68
- path: 'second-page',
69
- slug: 'test'
70
- },
71
- query: {},
72
- app: { model }
73
- } as FormRequest
74
- })
75
-
76
- describe('Properties', () => {
77
- it('returns path', () => {
78
- expect(controller1).toHaveProperty('path', '/first-page')
79
- expect(controller2).toHaveProperty('path', '/second-page')
80
- })
81
-
82
- it('returns href', () => {
83
- expect(controller1).toHaveProperty('href', '/test/first-page')
84
- expect(controller2).toHaveProperty('href', '/test/second-page')
85
- })
86
-
87
- it('returns keys', () => {
88
- expect(controller1).toHaveProperty('keys', ['yesNoField'])
89
- expect(controller2).toHaveProperty('keys', [
90
- 'dateField',
91
- 'dateField__day',
92
- 'dateField__month',
93
- 'dateField__year',
94
- 'multilineTextField'
95
- ])
96
- })
97
-
98
- it('returns the page section', () => {
99
- expect(controller1).toHaveProperty('section', undefined)
100
- expect(controller2).toHaveProperty('section', {
101
- name: 'marriage',
102
- title: 'Your marriage',
103
- hideTitle: false
104
- })
105
- })
106
- })
107
-
108
- describe('Path methods', () => {
109
- describe('Next getter', () => {
110
- it('returns page links', () => {
111
- expect(controller1).toHaveProperty('next', [
112
- { path: '/second-page' },
113
- { path: '/summary', condition: 'isPreviouslyMarried' }
114
- ])
115
-
116
- expect(controller2).toHaveProperty('next', [{ path: '/summary' }])
117
- })
118
-
119
- it('returns page links (none found)', () => {
120
- const pageDef1 = structuredClone(controller1.pageDef)
121
- const pageDef2 = structuredClone(controller2.pageDef)
122
-
123
- controller1.pageDef = pageDef1
124
- controller2.pageDef = pageDef2
125
-
126
- // @ts-expect-error - Allow invalid property for test
127
- controller1.pageDef.next = []
128
-
129
- // @ts-expect-error - Allow invalid property for test
130
- delete controller2.pageDef.next
131
-
132
- expect(controller1).toHaveProperty('next', [])
133
- expect(controller2).toHaveProperty('next', [])
134
- })
135
- })
136
- })
137
-
138
- describe('Component collection', () => {
139
- it('returns the components for the page', () => {
140
- const { components: components1 } = controller1.collection
141
- const { components: components2 } = controller2.collection
142
-
143
- expect(components1).toHaveLength(1)
144
- expect(components1[0].name).toBe('yesNoField')
145
-
146
- expect(components2).toHaveLength(3)
147
- expect(components2[0].name).toBe('detailsField')
148
- expect(components2[1].name).toBe('dateField')
149
- expect(components2[2].name).toBe('multilineTextField')
150
- })
151
-
152
- it('returns the fields for the page', () => {
153
- const { fields: fields1 } = controller1.collection
154
- const { fields: fields2 } = controller2.collection
155
-
156
- expect(fields1).toHaveLength(1)
157
- expect(fields1[0].name).toBe('yesNoField')
158
-
159
- expect(fields2).toHaveLength(2)
160
- expect(fields2[0].name).toBe('dateField')
161
- expect(fields2[1].name).toBe('multilineTextField')
162
- })
163
-
164
- it('returns the guidance for the page', () => {
165
- const { guidance: guidance1 } = controller1.collection
166
- const { guidance: guidance2 } = controller2.collection
167
-
168
- expect(guidance1).toHaveLength(0)
169
- expect(guidance2).toHaveLength(1)
170
- expect(guidance2[0].name).toBe('detailsField')
171
- })
172
- })
173
-
174
- describe('Component view models', () => {
175
- let viewModel1: FormPageViewModel
176
- let viewModel2: FormPageViewModel
177
-
178
- beforeEach(() => {
179
- viewModel1 = controller1.getViewModel(
180
- requestPage1,
181
- model.getFormContext(requestPage1, {})
182
- )
183
-
184
- viewModel2 = controller2.getViewModel(
185
- requestPage2,
186
- model.getFormContext(requestPage2, {})
187
- )
188
- })
189
-
190
- it('hides the page title for single form component pages', () => {
191
- // Page 1 hides page title (single component)
192
- expect(viewModel1).toHaveProperty('showTitle', false)
193
- expect(viewModel1).toHaveProperty('pageTitle', 'Previous marriages')
194
-
195
- // Page 2 shows page title (multiple components)
196
- expect(viewModel2).toHaveProperty('showTitle', true)
197
- expect(viewModel2).toHaveProperty(
198
- 'pageTitle',
199
- 'When will you get married?'
200
- )
201
- })
202
-
203
- it('returns the component view models for the page', () => {
204
- const { components: components1 } = viewModel1
205
- const { components: components2 } = viewModel2
206
-
207
- expect(components1).toHaveLength(1)
208
- expect(components2).toHaveLength(3)
209
-
210
- // Page 1, component 1, default label
211
- expect(components1[0].model).toHaveProperty('label', {
212
- text: 'Have you previously been married?'
213
- })
214
-
215
- // Page 1, component 1, optional legend
216
- expect(components1[0].model).toHaveProperty('fieldset', {
217
- legend: {
218
- text: 'Previous marriages',
219
- classes: 'govuk-fieldset__legend--l',
220
- isPageHeading: true
221
- }
222
- })
223
-
224
- // Page 2, component 1, content only
225
- expect(components2[0].model).toEqual({
226
- attributes: {},
227
- summaryHtml: 'Find out more',
228
- html: 'Some content goes here'
229
- })
230
-
231
- // Page 2, component 2, default label
232
- expect(components2[1].model).toHaveProperty('label', {
233
- text: 'Date of marriage',
234
- classes: 'govuk-label--m'
235
- })
236
-
237
- // Page 2, component 2, optional legend
238
- expect(components2[1].model).toHaveProperty('fieldset', {
239
- legend: {
240
- text: 'Date of marriage',
241
- classes: 'govuk-fieldset__legend--m'
242
- }
243
- })
244
-
245
- // Page 2, component 3, default label
246
- expect(components2[2].model).toHaveProperty('label', {
247
- text: 'Remarks',
248
- classes: 'govuk-label--m'
249
- })
250
-
251
- // Page 2, component 3, optional legend
252
- expect(components2[2].model).not.toHaveProperty('fieldset')
253
- })
254
- })
255
-
256
- describe('Condition evaluation context', () => {
257
- it('filters state by journey pages', () => {
258
- const { pages } = definitionConditionsComplex
259
-
260
- const model = new FormModel(definitionConditionsComplex, {
261
- basePath: 'test'
262
- })
263
-
264
- // Selected page appears after convergence and contains a conditional field
265
- // This is the page we're theoretically browsing to
266
- const controller = new QuestionPageController(model, pages[7])
267
-
268
- // The state below shows we said we had a UKPassport and entered details for an applicant
269
- const state: FormSubmissionState = {
270
- ukPassport: true,
271
- numberOfApplicants: 2,
272
- applicantOneFirstName: 'Enrique',
273
- applicantOneMiddleName: null,
274
- applicantOneLastName: 'Chase',
275
- applicantOneAddress__addressLine1: 'AddressLine1',
276
- applicantOneAddress__addressLine2: 'AddressLine2',
277
- applicantOneAddress__town: 'Town',
278
- applicantOneAddress__postcode: 'Postcode',
279
- applicantTwoFirstName: 'John',
280
- applicantTwoMiddleName: null,
281
- applicantTwoLastName: 'Doe',
282
- applicantTwoAddress__addressLine1: 'AddressLine1',
283
- applicantTwoAddress__addressLine2: 'AddressLine2',
284
- applicantTwoAddress__town: 'Town',
285
- applicantTwoAddress__postcode: 'Postcode'
286
- }
287
-
288
- let request = {
289
- method: 'get',
290
- url: new URL('http://example.com/test/applicant-one-address'),
291
- path: '/test/applicant-one-address',
292
- params: {
293
- path: 'applicant-one-address',
294
- slug: 'test'
295
- },
296
- query: {},
297
- app: { model }
298
- } satisfies FormContextRequest
299
-
300
- // Calculate our context based on the page we're attempting to load and the above state we provide
301
- let context = controller.model.getFormContext(request, state)
302
- const { evaluationState: stateBefore } = context
303
-
304
- // Our context should know our first applicant
305
- expect(stateBefore).toEqual({
306
- numberOfApplicants: 2,
307
- ukPassport: true,
308
- applicantOneFirstName: 'Enrique',
309
- applicantOneMiddleName: null,
310
- applicantOneLastName: 'Chase',
311
- applicantOneAddress: [
312
- 'AddressLine1',
313
- 'AddressLine2',
314
- 'Town',
315
- 'Postcode'
316
- ]
317
- })
318
-
319
- // Our context should know which pages are relevant
320
- expect(context.paths).toEqual([
321
- '/uk-passport',
322
- '/how-many-people',
323
- '/applicant-one-name',
324
- '/applicant-one-address'
325
- ])
326
-
327
- // Now mark that we don't have a UK Passport
328
- state.ukPassport = false
329
-
330
- request = {
331
- method: 'get',
332
- url: new URL('http://example.com/test/summary'),
333
- path: '/test/summary',
334
- params: {
335
- path: 'summary',
336
- slug: 'test'
337
- },
338
- query: {},
339
- app: { model }
340
- } satisfies FormContextRequest
341
-
342
- // And recalculate our context
343
- context = controller.model.getFormContext(request, state)
344
- const { evaluationState: stateAfter } = context
345
-
346
- // Our context should no longer list pages about our applicant
347
- expect(context.paths).toEqual([
348
- '/uk-passport',
349
- '/testconditions',
350
- '/summary'
351
- ])
352
-
353
- // Our context should no longer know anything about our applicant
354
- expect(stateAfter).not.toHaveProperty('numberOfApplicants')
355
- expect(stateAfter).not.toHaveProperty('applicantOneFirstName')
356
- expect(stateAfter).not.toHaveProperty('applicantOneMiddleName')
357
- expect(stateAfter).not.toHaveProperty('applicantOneLastName')
358
- expect(stateAfter).not.toHaveProperty('applicantOneAddress')
359
-
360
- expect(stateAfter).toEqual({
361
- ukPassport: false
362
- })
363
- })
364
-
365
- it('combines state values for date fields', () => {
366
- const { pages } = definitionConditionsDates
367
-
368
- const model = new FormModel(definitionConditionsDates, {
369
- basePath: 'test'
370
- })
371
-
372
- const controller = new QuestionPageController(model, pages[0])
373
-
374
- const request = {
375
- method: 'get',
376
- url: new URL('http://example.com/test/page-one'),
377
- path: '/test/page-one',
378
- params: {
379
- path: 'page-one',
380
- slug: 'test'
381
- },
382
- query: {},
383
- app: { model }
384
- } satisfies FormContextRequest
385
-
386
- const context = controller.model.getFormContext(request, {
387
- dateField__day: 5,
388
- dateField__month: 1,
389
- dateField__year: 2024
390
- })
391
-
392
- // Ensure dates are transformed to yyyy-MM-dd format
393
- expect(context.evaluationState).toHaveProperty('dateField', '2024-01-05')
394
-
395
- // Unlike relevant state which has the individual date parts
396
- expect(context.relevantState).not.toHaveProperty('dateField')
397
- expect(context.relevantState).toMatchObject({
398
- dateField__day: 5,
399
- dateField__month: 1,
400
- dateField__year: 2024
401
- })
402
- })
403
-
404
- it('filters on condition A', () => {
405
- const { pages } = conditionalReveal
406
-
407
- const model = new FormModel(conditionalReveal, {
408
- basePath: 'test'
409
- })
410
-
411
- const controller = new QuestionPageController(model, pages[0])
412
-
413
- // The state below shows we said we had a UKPassport and entered details for an applicant
414
- const state: FormSubmissionState = {}
415
-
416
- const request = {
417
- method: 'get',
418
- url: new URL('http://example.com/test/page-one'),
419
- path: '/test/first-page',
420
- params: {
421
- path: 'first-page',
422
- slug: 'test'
423
- },
424
- query: {},
425
- app: { model }
426
- } satisfies FormContextRequest
427
-
428
- const context = controller.model.getFormContext(request, state)
429
- const evaluationState = { animalType: 'Barn owl' }
430
-
431
- const viewModel = controller.getViewModel(request, context)
432
-
433
- const filtered = controller.filterConditionalComponents(
434
- viewModel,
435
- model,
436
- evaluationState
437
- )
438
-
439
- expect(filtered).toHaveLength(2)
440
- expect(filtered[0].model.content).toBe('This is info for Barn owls')
441
- expect(filtered[1].model.label?.text).toBe('Select from the list')
442
- expect(filtered[1].model.items).toEqual([
443
- {
444
- checked: false,
445
- condition: 'isBarnOwl',
446
- selected: false,
447
- text: 'Option 1',
448
- value: '1'
449
- },
450
- {
451
- checked: false,
452
- condition: 'isBarnOwl',
453
- selected: false,
454
- text: 'Option 2',
455
- value: '2'
456
- }
457
- ])
458
- })
459
-
460
- it('filters on condition B', () => {
461
- const { pages } = conditionalReveal
462
-
463
- const model = new FormModel(conditionalReveal, {
464
- basePath: 'test'
465
- })
466
-
467
- const controller = new QuestionPageController(model, pages[0])
468
-
469
- // The state below shows we said we had a UKPassport and entered details for an applicant
470
- const state: FormSubmissionState = {}
471
-
472
- const request = {
473
- method: 'get',
474
- url: new URL('http://example.com/test/page-one'),
475
- path: '/test/first-page',
476
- params: {
477
- path: 'first-page',
478
- slug: 'test'
479
- },
480
- query: {},
481
- app: { model }
482
- } satisfies FormContextRequest
483
-
484
- const context = controller.model.getFormContext(request, state)
485
- const evaluationState = { animalType: 'Swan' }
486
-
487
- const viewModel = controller.getViewModel(request, context)
488
-
489
- const filtered = controller.filterConditionalComponents(
490
- viewModel,
491
- model,
492
- evaluationState
493
- )
494
-
495
- expect(filtered).toHaveLength(2)
496
- expect(filtered[0].model.content).toBe('This is info for other breeds')
497
- expect(filtered[1].model.label?.text).toBe('Select from the list')
498
- expect(filtered[1].model.items).toEqual([
499
- {
500
- checked: false,
501
- condition: 'notBarnOwl',
502
- selected: false,
503
- text: 'Option 3',
504
- value: '3'
505
- },
506
- {
507
- checked: false,
508
- condition: 'notBarnOwl',
509
- selected: false,
510
- text: 'Option 4',
511
- value: '4'
512
- }
513
- ])
514
- })
515
- })
516
-
517
- describe('Form validation', () => {
518
- it('includes all field errors', () => {
519
- const result1 = controller1.collection.validate()
520
- const result2 = controller2.collection.validate()
521
-
522
- expect(result1.errors).toHaveLength(1)
523
- expect(result2.errors).toHaveLength(4)
524
- })
525
- })
526
-
527
- describe('Form journey', () => {
528
- let context: FormContext
529
- let contextNo: FormContext
530
- let contextYes: FormContext
531
-
532
- beforeEach(() => {
533
- // Empty state
534
- context = model.getFormContext(requestPage1, {})
535
-
536
- // Question 1: Selected 'No'
537
- contextNo = model.getFormContext(requestPage1, {
538
- yesNoField: false
539
- })
540
-
541
- // Question 1: Selected 'Yes'
542
- contextYes = model.getFormContext(requestPage1, {
543
- yesNoField: true
544
- })
545
- })
546
-
547
- describe('Next getter', () => {
548
- it('returns the next page links', () => {
549
- expect(controller1).toHaveProperty('next', [
550
- { path: '/second-page' },
551
- { path: '/summary', condition: 'isPreviouslyMarried' }
552
- ])
553
-
554
- expect(controller2).toHaveProperty('next', [{ path: '/summary' }])
555
- })
556
- })
557
-
558
- describe('Next', () => {
559
- it('returns the next page path', () => {
560
- expect(controller1.getNextPath(context)).toBe('/second-page')
561
- expect(controller1.getNextPath(contextNo)).toBe('/second-page')
562
- expect(controller1.getNextPath(contextYes)).toBe('/summary')
563
-
564
- expect(controller2.getNextPath(context)).toBe('/summary')
565
- expect(controller2.getNextPath(contextNo)).toBe('/summary')
566
- expect(controller2.getNextPath(contextYes)).toBe('/summary')
567
- })
568
- })
569
-
570
- describe('Summary', () => {
571
- it('returns the summary path', () => {
572
- expect(controller1.getSummaryPath()).toBe('/summary')
573
- expect(controller2.getSummaryPath()).toBe('/summary')
574
- })
575
- })
576
- })
577
-
578
- describe('Route handlers', () => {
579
- const response = {
580
- code: jest.fn().mockImplementation(() => response)
581
- }
582
-
583
- const h: Pick<ResponseToolkit, 'redirect' | 'view'> = {
584
- redirect: jest.fn().mockReturnValue(response),
585
- view: jest.fn()
586
- }
587
-
588
- it('returns default route options', () => {
589
- expect(controller1.getRouteOptions).toMatchObject({
590
- ext: {
591
- onPostHandler: {
592
- method: expect.any(Function)
593
- }
594
- }
595
- })
596
-
597
- expect(controller1.postRouteOptions).toMatchObject({
598
- ext: {
599
- onPostHandler: {
600
- method: expect.any(Function)
601
- }
602
- }
603
- })
604
- })
605
-
606
- it('supports GET route handler', async () => {
607
- const state: FormState = { yesNoField: false }
608
-
609
- for (const controller of [controller1, controller2]) {
610
- jest
611
- .spyOn(controller, 'hasMissingNotificationEmail')
612
- .mockResolvedValue(false)
613
-
614
- jest.spyOn(controller, 'getState').mockResolvedValue(state)
615
- }
616
-
617
- expect(() => controller1.makeGetRouteHandler()).not.toThrow()
618
- expect(() => controller1.makeGetRouteHandler()).toBeInstanceOf(Function)
619
-
620
- await controller1.makeGetRouteHandler()(
621
- requestPage1,
622
- model.getFormContext(requestPage1, {}),
623
- h
624
- )
625
-
626
- await controller2.makeGetRouteHandler()(
627
- requestPage2,
628
- model.getFormContext(requestPage2, {}),
629
- h
630
- )
631
-
632
- expect(h.view).toHaveBeenNthCalledWith(
633
- 1,
634
- controller1.viewName,
635
- expect.objectContaining({
636
- pageTitle: 'Previous marriages',
637
- sectionTitle: undefined
638
- })
639
- )
640
-
641
- expect(h.view).toHaveBeenNthCalledWith(
642
- 2,
643
- controller2.viewName,
644
- expect.objectContaining({
645
- pageTitle: 'When will you get married?',
646
- sectionTitle: 'Your marriage'
647
- })
648
- )
649
- })
650
- })
651
-
652
- describe('State', () => {
653
- beforeEach(() => {
654
- jest.spyOn(CacheService.prototype, 'getState')
655
- jest.spyOn(CacheService.prototype, 'setState')
656
-
657
- // Preview URL '?force'
658
- requestPage1.query.force = ''
659
- })
660
-
661
- describe('getState', () => {
662
- it('should skip get for preview URL direct access', async () => {
663
- const state = await controller1.getState(requestPage1)
664
-
665
- expect(state).toEqual({})
666
- expect(CacheService.prototype.getState).not.toHaveBeenCalled()
667
- })
668
- })
669
-
670
- describe('setState', () => {
671
- it('should skip set for preview URL direct access', async () => {
672
- const state: FormSubmissionState = { yesNoField: false }
673
- const updated = await controller1.setState(requestPage1, state)
674
-
675
- expect(updated).toBe(state)
676
- expect(CacheService.prototype.setState).not.toHaveBeenCalled()
677
- })
678
- })
679
-
680
- describe('mergeState', () => {
681
- it('should skip merge for preview URL direct access', async () => {
682
- const state: FormSubmissionState = {
683
- yesNoField: false
684
- }
685
-
686
- const update: FormSubmissionState = {
687
- dateField__day: 5,
688
- dateField__month: 1,
689
- dateField__year: 2024
690
- }
691
-
692
- const updated = await controller1.mergeState(
693
- requestPage1,
694
- state,
695
- update
696
- )
697
-
698
- expect(updated).toEqual({
699
- yesNoField: false,
700
- dateField__day: 5,
701
- dateField__month: 1,
702
- dateField__year: 2024
703
- })
704
-
705
- expect(CacheService.prototype.setState).not.toHaveBeenCalled()
706
- })
707
- })
708
- })
709
- })
710
-
711
- describe('QuestionPageController V2', () => {
712
- let page1Url: URL
713
- let page2Url: URL
714
-
715
- let model: FormModel
716
- let controller1: QuestionPageController
717
- let controller2: QuestionPageController
718
- let requestPage1: FormRequest
719
- let requestPage2: FormRequest
720
-
721
- beforeEach(() => {
722
- page1Url = new URL('http://example.com/test/first-page')
723
- page2Url = new URL('http://example.com/test/second-page')
724
-
725
- model = new FormModel(definitionConditionsBasicV2, {
726
- basePath: 'test'
727
- })
728
-
729
- controller1 = model.pages[0] // new QuestionPageController(model, page1)
730
- controller2 = model.pages[1] // new QuestionPageController(model, page2)
731
-
732
- requestPage1 = {
733
- method: 'get',
734
- url: page1Url,
735
- path: page1Url.pathname,
736
- params: {
737
- path: 'first-page',
738
- slug: 'test'
739
- },
740
- query: {},
741
- app: { model }
742
- } as FormRequest
743
-
744
- requestPage2 = {
745
- method: 'get',
746
- url: page2Url,
747
- path: page2Url.pathname,
748
- params: {
749
- path: 'second-page',
750
- slug: 'test'
751
- },
752
- query: {},
753
- app: { model }
754
- } as FormRequest
755
- })
756
-
757
- describe('Properties', () => {
758
- it('returns path', () => {
759
- expect(controller1).toHaveProperty('path', '/first-page')
760
- expect(controller2).toHaveProperty('path', '/second-page')
761
- })
762
-
763
- it('returns href', () => {
764
- expect(controller1).toHaveProperty('href', '/test/first-page')
765
- expect(controller2).toHaveProperty('href', '/test/second-page')
766
- })
767
-
768
- it('returns keys', () => {
769
- expect(controller1).toHaveProperty('keys', ['yesNoField'])
770
- expect(controller2).toHaveProperty('keys', [
771
- 'dateField',
772
- 'dateField__day',
773
- 'dateField__month',
774
- 'dateField__year',
775
- 'multilineTextField'
776
- ])
777
- })
778
-
779
- it('returns the page section', () => {
780
- expect(controller1).toHaveProperty('section', undefined)
781
- expect(controller2).toHaveProperty('section', {
782
- name: 'marriage',
783
- title: 'Your marriage',
784
- hideTitle: false
785
- })
786
- })
787
- })
788
-
789
- describe('Path methods', () => {
790
- describe('Next getter', () => {
791
- it('returns page links', () => {
792
- expect(controller1).toHaveProperty('next', [])
793
-
794
- expect(controller2).toHaveProperty('next', [])
795
- })
796
-
797
- it('returns page links (none found)', () => {
798
- const pageDef1 = structuredClone(controller1.pageDef)
799
- const pageDef2 = structuredClone(controller2.pageDef)
800
-
801
- controller1.pageDef = pageDef1
802
- controller2.pageDef = pageDef2
803
-
804
- // @ts-expect-error - Allow invalid property for test
805
- controller1.pageDef.next = []
806
-
807
- // @ts-expect-error - Allow invalid property for test
808
- delete controller2.pageDef.next
809
-
810
- expect(controller1).toHaveProperty('next', [])
811
- expect(controller2).toHaveProperty('next', [])
812
- })
813
- })
814
- })
815
-
816
- describe('Component collection', () => {
817
- it('returns the components for the page', () => {
818
- const { components: components1 } = controller1.collection
819
- const { components: components2 } = controller2.collection
820
-
821
- expect(components1).toHaveLength(1)
822
- expect(components1[0].name).toBe('yesNoField')
823
-
824
- expect(components2).toHaveLength(3)
825
- expect(components2[0].name).toBe('detailsField')
826
- expect(components2[1].name).toBe('dateField')
827
- expect(components2[2].name).toBe('multilineTextField')
828
- })
829
-
830
- it('returns the fields for the page', () => {
831
- const { fields: fields1 } = controller1.collection
832
- const { fields: fields2 } = controller2.collection
833
-
834
- expect(fields1).toHaveLength(1)
835
- expect(fields1[0].name).toBe('yesNoField')
836
-
837
- expect(fields2).toHaveLength(2)
838
- expect(fields2[0].name).toBe('dateField')
839
- expect(fields2[1].name).toBe('multilineTextField')
840
- })
841
-
842
- it('returns the guidance for the page', () => {
843
- const { guidance: guidance1 } = controller1.collection
844
- const { guidance: guidance2 } = controller2.collection
845
-
846
- expect(guidance1).toHaveLength(0)
847
- expect(guidance2).toHaveLength(1)
848
- expect(guidance2[0].name).toBe('detailsField')
849
- })
850
- })
851
-
852
- describe('Component view models', () => {
853
- let viewModel1: FormPageViewModel
854
- let viewModel2: FormPageViewModel
855
-
856
- beforeEach(() => {
857
- viewModel1 = controller1.getViewModel(
858
- requestPage1,
859
- model.getFormContext(requestPage1, {})
860
- )
861
-
862
- viewModel2 = controller2.getViewModel(
863
- requestPage2,
864
- model.getFormContext(requestPage2, {})
865
- )
866
- })
867
-
868
- it('hides the page title for single form component pages', () => {
869
- // Page 1 hides page title (single component)
870
- expect(viewModel1).toHaveProperty('showTitle', false)
871
- expect(viewModel1).toHaveProperty('pageTitle', 'Previous marriages')
872
-
873
- // Page 2 shows page title (multiple components)
874
- expect(viewModel2).toHaveProperty('showTitle', true)
875
- expect(viewModel2).toHaveProperty(
876
- 'pageTitle',
877
- 'When will you get married?'
878
- )
879
- })
880
-
881
- it('returns the component view models for the page', () => {
882
- const { components: components1 } = viewModel1
883
- const { components: components2 } = viewModel2
884
-
885
- expect(components1).toHaveLength(1)
886
- expect(components2).toHaveLength(3)
887
-
888
- // Page 1, component 1, default label
889
- expect(components1[0].model).toHaveProperty('label', {
890
- text: 'Have you previously been married?'
891
- })
892
-
893
- // Page 1, component 1, optional legend
894
- expect(components1[0].model).toHaveProperty('fieldset', {
895
- legend: {
896
- text: 'Previous marriages',
897
- classes: 'govuk-fieldset__legend--l',
898
- isPageHeading: true
899
- }
900
- })
901
-
902
- // Page 2, component 1, content only
903
- expect(components2[0].model).toEqual({
904
- attributes: {},
905
- summaryHtml: 'Find out more',
906
- html: 'Some content goes here'
907
- })
908
-
909
- // Page 2, component 2, default label
910
- expect(components2[1].model).toHaveProperty('label', {
911
- text: 'Date of marriage',
912
- classes: 'govuk-label--m'
913
- })
914
-
915
- // Page 2, component 2, optional legend
916
- expect(components2[1].model).toHaveProperty('fieldset', {
917
- legend: {
918
- text: 'Date of marriage',
919
- classes: 'govuk-fieldset__legend--m'
920
- }
921
- })
922
-
923
- // Page 2, component 3, default label
924
- expect(components2[2].model).toHaveProperty('label', {
925
- text: 'Remarks',
926
- classes: 'govuk-label--m'
927
- })
928
-
929
- // Page 2, component 3, optional legend
930
- expect(components2[2].model).not.toHaveProperty('fieldset')
931
- })
932
- })
933
-
934
- describe('Condition evaluation context', () => {
935
- it('filters state by journey pages', () => {
936
- const { pages } = definitionConditionsComplex
937
-
938
- const model = new FormModel(definitionConditionsComplex, {
939
- basePath: 'test'
940
- })
941
-
942
- // Selected page appears after convergence and contains a conditional field
943
- // This is the page we're theoretically browsing to
944
- const controller = new QuestionPageController(model, pages[7])
945
-
946
- // The state below shows we said we had a UKPassport and entered details for an applicant
947
- const state: FormSubmissionState = {
948
- ukPassport: true,
949
- numberOfApplicants: 2,
950
- applicantOneFirstName: 'Enrique',
951
- applicantOneMiddleName: null,
952
- applicantOneLastName: 'Chase',
953
- applicantOneAddress__addressLine1: 'AddressLine1',
954
- applicantOneAddress__addressLine2: 'AddressLine2',
955
- applicantOneAddress__town: 'Town',
956
- applicantOneAddress__postcode: 'Postcode',
957
- applicantTwoFirstName: 'John',
958
- applicantTwoMiddleName: null,
959
- applicantTwoLastName: 'Doe',
960
- applicantTwoAddress__addressLine1: 'AddressLine1',
961
- applicantTwoAddress__addressLine2: 'AddressLine2',
962
- applicantTwoAddress__town: 'Town',
963
- applicantTwoAddress__postcode: 'Postcode'
964
- }
965
-
966
- let request = {
967
- method: 'get',
968
- url: new URL('http://example.com/test/applicant-one-address'),
969
- path: '/test/applicant-one-address',
970
- params: {
971
- path: 'applicant-one-address',
972
- slug: 'test'
973
- },
974
- query: {},
975
- app: { model }
976
- } satisfies FormContextRequest
977
-
978
- // Calculate our context based on the page we're attempting to load and the above state we provide
979
- let context = controller.model.getFormContext(request, state)
980
- const { evaluationState: stateBefore } = context
981
-
982
- // Our context should know our first applicant
983
- expect(stateBefore).toEqual({
984
- numberOfApplicants: 2,
985
- ukPassport: true,
986
- applicantOneFirstName: 'Enrique',
987
- applicantOneMiddleName: null,
988
- applicantOneLastName: 'Chase',
989
- applicantOneAddress: [
990
- 'AddressLine1',
991
- 'AddressLine2',
992
- 'Town',
993
- 'Postcode'
994
- ]
995
- })
996
-
997
- // Our context should know which pages are relevant
998
- expect(context.paths).toEqual([
999
- '/uk-passport',
1000
- '/how-many-people',
1001
- '/applicant-one-name',
1002
- '/applicant-one-address'
1003
- ])
1004
-
1005
- // Now mark that we don't have a UK Passport
1006
- state.ukPassport = false
1007
-
1008
- request = {
1009
- method: 'get',
1010
- url: new URL('http://example.com/test/summary'),
1011
- path: '/test/summary',
1012
- params: {
1013
- path: 'summary',
1014
- slug: 'test'
1015
- },
1016
- query: {},
1017
- app: { model }
1018
- } satisfies FormContextRequest
1019
-
1020
- // And recalculate our context
1021
- context = controller.model.getFormContext(request, state)
1022
- const { evaluationState: stateAfter } = context
1023
-
1024
- // Our context should no longer list pages about our applicant
1025
- expect(context.paths).toEqual([
1026
- '/uk-passport',
1027
- '/testconditions',
1028
- '/summary'
1029
- ])
1030
-
1031
- // Our context should no longer know anything about our applicant
1032
- expect(stateAfter).not.toHaveProperty('numberOfApplicants')
1033
- expect(stateAfter).not.toHaveProperty('applicantOneFirstName')
1034
- expect(stateAfter).not.toHaveProperty('applicantOneMiddleName')
1035
- expect(stateAfter).not.toHaveProperty('applicantOneLastName')
1036
- expect(stateAfter).not.toHaveProperty('applicantOneAddress')
1037
-
1038
- expect(stateAfter).toEqual({
1039
- ukPassport: false
1040
- })
1041
- })
1042
-
1043
- it('combines state values for date fields', () => {
1044
- const { pages } = definitionConditionsDates
1045
-
1046
- const model = new FormModel(definitionConditionsDates, {
1047
- basePath: 'test'
1048
- })
1049
-
1050
- const controller = new QuestionPageController(model, pages[0])
1051
-
1052
- const request = {
1053
- method: 'get',
1054
- url: new URL('http://example.com/test/page-one'),
1055
- path: '/test/page-one',
1056
- params: {
1057
- path: 'page-one',
1058
- slug: 'test'
1059
- },
1060
- query: {},
1061
- app: { model }
1062
- } satisfies FormContextRequest
1063
-
1064
- const context = controller.model.getFormContext(request, {
1065
- dateField__day: 5,
1066
- dateField__month: 1,
1067
- dateField__year: 2024
1068
- })
1069
-
1070
- // Ensure dates are transformed to yyyy-MM-dd format
1071
- expect(context.evaluationState).toHaveProperty('dateField', '2024-01-05')
1072
-
1073
- // Unlike relevant state which has the individual date parts
1074
- expect(context.relevantState).not.toHaveProperty('dateField')
1075
- expect(context.relevantState).toMatchObject({
1076
- dateField__day: 5,
1077
- dateField__month: 1,
1078
- dateField__year: 2024
1079
- })
1080
- })
1081
- })
1082
-
1083
- describe('Form validation', () => {
1084
- it('includes all field errors', () => {
1085
- const result1 = controller1.collection.validate()
1086
- const result2 = controller2.collection.validate()
1087
-
1088
- expect(result1.errors).toHaveLength(1)
1089
- expect(result2.errors).toHaveLength(4)
1090
- })
1091
- })
1092
-
1093
- describe('Form journey', () => {
1094
- let context: FormContext
1095
- let contextNo: FormContext
1096
- let contextYes: FormContext
1097
-
1098
- beforeEach(() => {
1099
- // Empty state
1100
- context = model.getFormContext(requestPage1, {})
1101
-
1102
- // Question 1: Selected 'No'
1103
- contextNo = model.getFormContext(requestPage1, {
1104
- yesNoField: false
1105
- })
1106
-
1107
- // Question 1: Selected 'Yes'
1108
- contextYes = model.getFormContext(requestPage1, {
1109
- yesNoField: true
1110
- })
1111
- })
1112
-
1113
- describe('Next', () => {
1114
- it('returns the next page path', () => {
1115
- expect(controller1.getNextPath(context)).toBe('/summary')
1116
- expect(controller1.getNextPath(contextNo)).toBe('/second-page')
1117
- expect(controller1.getNextPath(contextYes)).toBe('/summary')
1118
-
1119
- expect(controller2.getNextPath(context)).toBe('/summary')
1120
- expect(controller2.getNextPath(contextNo)).toBe('/summary')
1121
- expect(controller2.getNextPath(contextYes)).toBe('/summary')
1122
- })
1123
- })
1124
-
1125
- describe('Summary', () => {
1126
- it('returns the summary path', () => {
1127
- expect(controller1.getSummaryPath()).toBe('/summary')
1128
- expect(controller2.getSummaryPath()).toBe('/summary')
1129
- })
1130
- })
1131
- })
1132
-
1133
- describe('Route handlers', () => {
1134
- const response = {
1135
- code: jest.fn().mockImplementation(() => response)
1136
- }
1137
-
1138
- const h: Pick<ResponseToolkit, 'redirect' | 'view'> = {
1139
- redirect: jest.fn().mockReturnValue(response),
1140
- view: jest.fn()
1141
- }
1142
-
1143
- it('returns default route options', () => {
1144
- expect(controller1.getRouteOptions).toMatchObject({
1145
- ext: {
1146
- onPostHandler: {
1147
- method: expect.any(Function)
1148
- }
1149
- }
1150
- })
1151
-
1152
- expect(controller1.postRouteOptions).toMatchObject({
1153
- ext: {
1154
- onPostHandler: {
1155
- method: expect.any(Function)
1156
- }
1157
- }
1158
- })
1159
- })
1160
-
1161
- it('supports GET route handler', async () => {
1162
- const state: FormState = { yesNoField: false }
1163
-
1164
- for (const controller of [controller1, controller2]) {
1165
- jest
1166
- .spyOn(controller, 'hasMissingNotificationEmail')
1167
- .mockResolvedValue(false)
1168
-
1169
- jest.spyOn(controller, 'getState').mockResolvedValue(state)
1170
- }
1171
-
1172
- expect(() => controller1.makeGetRouteHandler()).not.toThrow()
1173
- expect(() => controller1.makeGetRouteHandler()).toBeInstanceOf(Function)
1174
-
1175
- await controller1.makeGetRouteHandler()(
1176
- requestPage1,
1177
- model.getFormContext(requestPage1, {}),
1178
- h
1179
- )
1180
-
1181
- await controller2.makeGetRouteHandler()(
1182
- requestPage2,
1183
- model.getFormContext(requestPage2, {}),
1184
- h
1185
- )
1186
-
1187
- expect(h.view).toHaveBeenNthCalledWith(
1188
- 1,
1189
- controller1.viewName,
1190
- expect.objectContaining({
1191
- pageTitle: 'Previous marriages',
1192
- sectionTitle: undefined
1193
- })
1194
- )
1195
-
1196
- expect(h.view).toHaveBeenNthCalledWith(
1197
- 2,
1198
- controller2.viewName,
1199
- expect.objectContaining({
1200
- pageTitle: 'When will you get married?',
1201
- sectionTitle: 'Your marriage'
1202
- })
1203
- )
1204
- })
1205
- })
1206
-
1207
- describe('State', () => {
1208
- beforeEach(() => {
1209
- jest.spyOn(CacheService.prototype, 'getState')
1210
- jest.spyOn(CacheService.prototype, 'setState')
1211
-
1212
- // Preview URL '?force'
1213
- requestPage1.query.force = ''
1214
- })
1215
-
1216
- describe('getState', () => {
1217
- it('should skip get for preview URL direct access', async () => {
1218
- const state = await controller1.getState(requestPage1)
1219
-
1220
- expect(state).toEqual({})
1221
- expect(CacheService.prototype.getState).not.toHaveBeenCalled()
1222
- })
1223
- })
1224
-
1225
- describe('setState', () => {
1226
- it('should skip set for preview URL direct access', async () => {
1227
- const state: FormSubmissionState = { yesNoField: false }
1228
- const updated = await controller1.setState(requestPage1, state)
1229
-
1230
- expect(updated).toBe(state)
1231
- expect(CacheService.prototype.setState).not.toHaveBeenCalled()
1232
- })
1233
- })
1234
-
1235
- describe('mergeState', () => {
1236
- it('should skip merge for preview URL direct access', async () => {
1237
- const state: FormSubmissionState = {
1238
- yesNoField: false
1239
- }
1240
-
1241
- const update: FormSubmissionState = {
1242
- dateField__day: 5,
1243
- dateField__month: 1,
1244
- dateField__year: 2024
1245
- }
1246
-
1247
- const updated = await controller1.mergeState(
1248
- requestPage1,
1249
- state,
1250
- update
1251
- )
1252
-
1253
- expect(updated).toEqual({
1254
- yesNoField: false,
1255
- dateField__day: 5,
1256
- dateField__month: 1,
1257
- dateField__year: 2024
1258
- })
1259
-
1260
- expect(CacheService.prototype.setState).not.toHaveBeenCalled()
1261
- })
1262
- })
1263
- })
1264
- })