@defra/forms-engine-plugin 0.0.4 → 0.0.6

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 (260) hide show
  1. package/.server/server/index.js +0 -4
  2. package/.server/server/index.js.map +1 -1
  3. package/.server/server/plugins/engine/helpers.js +3 -0
  4. package/.server/server/plugins/engine/helpers.js.map +1 -1
  5. package/.server/server/plugins/engine/index.js +27 -1
  6. package/.server/server/plugins/engine/index.js.map +1 -1
  7. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +2 -4
  8. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  9. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +4 -10
  10. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  11. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +2 -3
  12. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  13. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -4
  14. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  15. package/.server/server/plugins/engine/plugin.js +65 -6
  16. package/.server/server/plugins/engine/plugin.js.map +1 -1
  17. package/.server/server/plugins/engine/types.js.map +1 -1
  18. package/.server/server/{views → plugins/engine/views}/components/service-banner/template.test.js +1 -1
  19. package/.server/server/plugins/engine/views/components/service-banner/template.test.js.map +1 -0
  20. package/.server/server/{views → plugins/engine/views}/components/tag-env/template.test.js +1 -1
  21. package/.server/server/plugins/engine/views/components/tag-env/template.test.js.map +1 -0
  22. package/.server/server/services/cacheService.js +5 -2
  23. package/.server/server/services/cacheService.js.map +1 -1
  24. package/.server/typings/hapi/index.d.js.map +1 -1
  25. package/README.md +215 -4
  26. package/package.json +3 -3
  27. package/src/client/javascripts/application.js +87 -0
  28. package/src/client/javascripts/file-upload.js +386 -0
  29. package/src/client/stylesheets/_code.scss +33 -0
  30. package/src/client/stylesheets/_govuk-frontend.scss +4 -0
  31. package/src/client/stylesheets/_prose.scss +56 -0
  32. package/src/client/stylesheets/_service-banner.scss +24 -0
  33. package/src/client/stylesheets/_summary-list.scss +28 -0
  34. package/src/client/stylesheets/_tag-env.scss +24 -0
  35. package/src/client/stylesheets/application.scss +14 -0
  36. package/src/common/cookies.js +58 -0
  37. package/src/common/cookies.test.js +23 -0
  38. package/src/common/types.js +5 -0
  39. package/src/config/index.ts +271 -0
  40. package/src/index.ts +31 -0
  41. package/src/server/common/helpers/logging/logger-options.test.ts +50 -0
  42. package/src/server/common/helpers/logging/logger-options.ts +46 -0
  43. package/src/server/common/helpers/logging/logger.ts +7 -0
  44. package/src/server/common/helpers/logging/request-logger.ts +9 -0
  45. package/src/server/common/helpers/logging/request-tracing.js +10 -0
  46. package/src/server/common/helpers/redis-client.js +70 -0
  47. package/src/server/constants.js +1 -0
  48. package/src/server/forms/README.md +10 -0
  49. package/src/server/forms/components.json +1015 -0
  50. package/src/server/forms/report-a-terrorist.json +270 -0
  51. package/src/server/forms/runner-components-test.json +365 -0
  52. package/src/server/forms/test.json +581 -0
  53. package/src/server/index.test.ts +582 -0
  54. package/src/server/index.ts +135 -0
  55. package/src/server/plugins/blankie.test.ts +73 -0
  56. package/src/server/plugins/blankie.ts +48 -0
  57. package/src/server/plugins/crumb.ts +20 -0
  58. package/src/server/plugins/engine/README.md +87 -0
  59. package/src/server/plugins/engine/components/AutocompleteField.test.ts +294 -0
  60. package/src/server/plugins/engine/components/AutocompleteField.ts +49 -0
  61. package/src/server/plugins/engine/components/CheckboxesField.test.ts +379 -0
  62. package/src/server/plugins/engine/components/CheckboxesField.ts +106 -0
  63. package/src/server/plugins/engine/components/ComponentBase.ts +97 -0
  64. package/src/server/plugins/engine/components/ComponentCollection.ts +278 -0
  65. package/src/server/plugins/engine/components/DatePartsField.test.ts +822 -0
  66. package/src/server/plugins/engine/components/DatePartsField.ts +264 -0
  67. package/src/server/plugins/engine/components/Details.test.ts +49 -0
  68. package/src/server/plugins/engine/components/Details.ts +30 -0
  69. package/src/server/plugins/engine/components/EmailAddressField.test.ts +395 -0
  70. package/src/server/plugins/engine/components/EmailAddressField.ts +55 -0
  71. package/src/server/plugins/engine/components/FileUploadField.test.ts +778 -0
  72. package/src/server/plugins/engine/components/FileUploadField.ts +262 -0
  73. package/src/server/plugins/engine/components/FormComponent.ts +249 -0
  74. package/src/server/plugins/engine/components/Html.test.ts +48 -0
  75. package/src/server/plugins/engine/components/Html.ts +29 -0
  76. package/src/server/plugins/engine/components/InsetText.test.ts +48 -0
  77. package/src/server/plugins/engine/components/InsetText.ts +27 -0
  78. package/src/server/plugins/engine/components/List.test.ts +76 -0
  79. package/src/server/plugins/engine/components/List.ts +72 -0
  80. package/src/server/plugins/engine/components/ListFormComponent.ts +140 -0
  81. package/src/server/plugins/engine/components/MonthYearField.test.ts +567 -0
  82. package/src/server/plugins/engine/components/MonthYearField.ts +222 -0
  83. package/src/server/plugins/engine/components/MultilineTextField.test.ts +558 -0
  84. package/src/server/plugins/engine/components/MultilineTextField.ts +138 -0
  85. package/src/server/plugins/engine/components/NumberField.test.ts +701 -0
  86. package/src/server/plugins/engine/components/NumberField.ts +163 -0
  87. package/src/server/plugins/engine/components/RadiosField.test.ts +288 -0
  88. package/src/server/plugins/engine/components/RadiosField.ts +24 -0
  89. package/src/server/plugins/engine/components/SelectField.test.ts +288 -0
  90. package/src/server/plugins/engine/components/SelectField.ts +47 -0
  91. package/src/server/plugins/engine/components/SelectionControlField.ts +43 -0
  92. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +356 -0
  93. package/src/server/plugins/engine/components/TelephoneNumberField.ts +67 -0
  94. package/src/server/plugins/engine/components/TextField.test.ts +489 -0
  95. package/src/server/plugins/engine/components/TextField.ts +96 -0
  96. package/src/server/plugins/engine/components/UkAddressField.test.ts +623 -0
  97. package/src/server/plugins/engine/components/UkAddressField.ts +172 -0
  98. package/src/server/plugins/engine/components/YesNoField.test.ts +248 -0
  99. package/src/server/plugins/engine/components/YesNoField.ts +31 -0
  100. package/src/server/plugins/engine/components/constants.ts +1 -0
  101. package/src/server/plugins/engine/components/helpers.ts +330 -0
  102. package/src/server/plugins/engine/components/index.ts +24 -0
  103. package/src/server/plugins/engine/components/types.ts +117 -0
  104. package/src/server/plugins/engine/configureEnginePlugin.ts +47 -0
  105. package/src/server/plugins/engine/helpers.test.ts +791 -0
  106. package/src/server/plugins/engine/helpers.ts +384 -0
  107. package/src/server/plugins/engine/index.ts +47 -0
  108. package/src/server/plugins/engine/models/FormModel.test.ts +42 -0
  109. package/src/server/plugins/engine/models/FormModel.ts +443 -0
  110. package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
  111. package/src/server/plugins/engine/models/Section.ts +0 -0
  112. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +209 -0
  113. package/src/server/plugins/engine/models/SummaryViewModel.ts +220 -0
  114. package/src/server/plugins/engine/models/index.ts +2 -0
  115. package/src/server/plugins/engine/models/types.ts +114 -0
  116. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +143 -0
  117. package/src/server/plugins/engine/outputFormatters/human/v1.ts +73 -0
  118. package/src/server/plugins/engine/outputFormatters/index.test.ts +17 -0
  119. package/src/server/plugins/engine/outputFormatters/index.ts +44 -0
  120. package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +229 -0
  121. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +140 -0
  122. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +229 -0
  123. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +153 -0
  124. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1116 -0
  125. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +447 -0
  126. package/src/server/plugins/engine/pageControllers/PageController.test.ts +205 -0
  127. package/src/server/plugins/engine/pageControllers/PageController.ts +176 -0
  128. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1264 -0
  129. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +565 -0
  130. package/src/server/plugins/engine/pageControllers/README.md +28 -0
  131. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +264 -0
  132. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +458 -0
  133. package/src/server/plugins/engine/pageControllers/StartPageController.ts +18 -0
  134. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +51 -0
  135. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +262 -0
  136. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +28 -0
  137. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +19 -0
  138. package/src/server/plugins/engine/pageControllers/helpers.test.ts +198 -0
  139. package/src/server/plugins/engine/pageControllers/helpers.ts +101 -0
  140. package/src/server/plugins/engine/pageControllers/index.ts +10 -0
  141. package/src/server/plugins/engine/pageControllers/validationOptions.ts +89 -0
  142. package/src/server/plugins/engine/plugin.ts +753 -0
  143. package/src/server/plugins/engine/services/formSubmissionService.js +46 -0
  144. package/src/server/plugins/engine/services/formsService.js +46 -0
  145. package/src/server/plugins/engine/services/formsService.test.js +90 -0
  146. package/src/server/plugins/engine/services/index.js +3 -0
  147. package/src/server/plugins/engine/services/notifyService.test.ts +132 -0
  148. package/src/server/plugins/engine/services/notifyService.ts +64 -0
  149. package/src/server/plugins/engine/services/uploadService.js +60 -0
  150. package/src/server/plugins/engine/types.ts +317 -0
  151. package/src/server/plugins/engine/views/components/autocompletefield.html +5 -0
  152. package/src/server/plugins/engine/views/components/checkboxesfield.html +5 -0
  153. package/src/server/plugins/engine/views/components/datepartsfield.html +5 -0
  154. package/src/server/plugins/engine/views/components/debug/macro.njk +3 -0
  155. package/src/server/plugins/engine/views/components/debug/template.njk +13 -0
  156. package/src/server/plugins/engine/views/components/details.html +6 -0
  157. package/src/server/plugins/engine/views/components/emailaddressfield.html +5 -0
  158. package/src/server/plugins/engine/views/components/fileuploadfield-key.html +8 -0
  159. package/src/server/plugins/engine/views/components/fileuploadfield-value.html +3 -0
  160. package/src/server/plugins/engine/views/components/fileuploadfield.html +24 -0
  161. package/src/server/plugins/engine/views/components/html.html +3 -0
  162. package/src/server/plugins/engine/views/components/insettext.html +7 -0
  163. package/src/server/plugins/engine/views/components/list.html +36 -0
  164. package/src/server/plugins/engine/views/components/monthyearfield.html +5 -0
  165. package/src/server/plugins/engine/views/components/multilinetextfield.html +10 -0
  166. package/src/server/plugins/engine/views/components/numberfield.html +5 -0
  167. package/src/server/plugins/engine/views/components/radiosfield.html +5 -0
  168. package/src/server/plugins/engine/views/components/selectfield.html +5 -0
  169. package/src/server/plugins/engine/views/components/service-banner/macro.njk +3 -0
  170. package/src/server/plugins/engine/views/components/service-banner/template.njk +20 -0
  171. package/src/server/plugins/engine/views/components/service-banner/template.test.js +43 -0
  172. package/src/server/plugins/engine/views/components/tag-env/macro.njk +3 -0
  173. package/src/server/plugins/engine/views/components/tag-env/template.njk +30 -0
  174. package/src/server/plugins/engine/views/components/tag-env/template.test.js +66 -0
  175. package/src/server/plugins/engine/views/components/telephonenumberfield.html +5 -0
  176. package/src/server/plugins/engine/views/components/textfield.html +5 -0
  177. package/src/server/plugins/engine/views/components/ukaddressfield.html +25 -0
  178. package/src/server/plugins/engine/views/components/yesnofield.html +5 -0
  179. package/src/server/plugins/engine/views/confirmation.html +19 -0
  180. package/src/server/plugins/engine/views/file-upload.html +45 -0
  181. package/src/server/plugins/engine/views/index.html +39 -0
  182. package/src/server/plugins/engine/views/item-delete.html +56 -0
  183. package/src/server/plugins/engine/views/layout.html +199 -0
  184. package/src/server/plugins/engine/views/partials/components.html +6 -0
  185. package/src/server/plugins/engine/views/partials/conditional-components.html +3 -0
  186. package/src/server/plugins/engine/views/partials/debug.html +44 -0
  187. package/src/server/plugins/engine/views/partials/form.html +15 -0
  188. package/src/server/plugins/engine/views/partials/heading.html +16 -0
  189. package/src/server/plugins/engine/views/partials/preview-banner.html +32 -0
  190. package/src/server/plugins/engine/views/partials/preview-banner.test.js +122 -0
  191. package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +10 -0
  192. package/src/server/plugins/engine/views/repeat-list-summary.html +53 -0
  193. package/src/server/plugins/engine/views/summary.html +50 -0
  194. package/src/server/plugins/errorPages.ts +58 -0
  195. package/src/server/plugins/nunjucks/context.js +88 -0
  196. package/src/server/plugins/nunjucks/context.test.js +142 -0
  197. package/src/server/plugins/nunjucks/enviroment.test.js +201 -0
  198. package/src/server/plugins/nunjucks/environment.js +116 -0
  199. package/src/server/plugins/nunjucks/filters/answer.js +27 -0
  200. package/src/server/plugins/nunjucks/filters/answer.test.js +89 -0
  201. package/src/server/plugins/nunjucks/filters/evaluate.js +21 -0
  202. package/src/server/plugins/nunjucks/filters/field.js +28 -0
  203. package/src/server/plugins/nunjucks/filters/field.test.js +75 -0
  204. package/src/server/plugins/nunjucks/filters/highlight.js +11 -0
  205. package/src/server/plugins/nunjucks/filters/href.js +30 -0
  206. package/src/server/plugins/nunjucks/filters/href.test.js +80 -0
  207. package/src/server/plugins/nunjucks/filters/index.js +8 -0
  208. package/src/server/plugins/nunjucks/filters/inspect.js +15 -0
  209. package/src/server/plugins/nunjucks/filters/page.js +24 -0
  210. package/src/server/plugins/nunjucks/filters/page.test.js +65 -0
  211. package/src/server/plugins/nunjucks/index.js +3 -0
  212. package/src/server/plugins/nunjucks/plugin.js +40 -0
  213. package/src/server/plugins/nunjucks/render.js +42 -0
  214. package/src/server/plugins/nunjucks/types.js +40 -0
  215. package/src/server/plugins/pulse.ts +11 -0
  216. package/src/server/plugins/router.ts +201 -0
  217. package/src/server/plugins/session.ts +28 -0
  218. package/src/server/routes/health.js +13 -0
  219. package/src/server/routes/health.test.js +35 -0
  220. package/src/server/routes/index.test.ts +125 -0
  221. package/src/server/routes/index.ts +2 -0
  222. package/src/server/routes/public.ts +47 -0
  223. package/src/server/routes/types.ts +48 -0
  224. package/src/server/schemas/index.ts +34 -0
  225. package/src/server/secure-context.js +43 -0
  226. package/src/server/services/cacheService.test.ts +277 -0
  227. package/src/server/services/cacheService.ts +138 -0
  228. package/src/server/services/httpService.test.js +491 -0
  229. package/src/server/services/httpService.ts +50 -0
  230. package/src/server/services/index.ts +1 -0
  231. package/src/server/types.ts +54 -0
  232. package/src/server/utils/notify.test.ts +37 -0
  233. package/src/server/utils/notify.ts +50 -0
  234. package/src/server/utils/secure-context/get-trust-store-certs.js +11 -0
  235. package/src/server/utils/secure-context/get-trust-store-certs.test.js +19 -0
  236. package/src/server/utils/utils.js +24 -0
  237. package/src/server/utils/utils.test.js +54 -0
  238. package/src/server/views/404.html +16 -0
  239. package/src/server/views/500.html +19 -0
  240. package/src/server/views/help/accessibility-statement.html +58 -0
  241. package/src/server/views/help/cookie-preferences.html +57 -0
  242. package/src/server/views/help/cookies.html +71 -0
  243. package/src/server/views/help/get-support.html +37 -0
  244. package/src/server/views/help/privacy-notice.html +68 -0
  245. package/src/server/views/help/terms-and-conditions.html +83 -0
  246. package/src/typings/hapi/index.d.ts +87 -0
  247. package/src/typings/hapi-tracing/index.d.ts +6 -0
  248. package/src/typings/index.d.ts +3 -0
  249. package/src/typings/joi/index.d.ts +22 -0
  250. package/.server/server/views/components/service-banner/template.test.js.map +0 -1
  251. package/.server/server/views/components/tag-env/template.test.js.map +0 -1
  252. /package/.server/server/{views → plugins/engine/views}/components/debug/macro.njk +0 -0
  253. /package/.server/server/{views → plugins/engine/views}/components/debug/template.njk +0 -0
  254. /package/.server/server/{views → plugins/engine/views}/components/service-banner/macro.njk +0 -0
  255. /package/.server/server/{views → plugins/engine/views}/components/service-banner/template.njk +0 -0
  256. /package/.server/server/{views → plugins/engine/views}/components/tag-env/macro.njk +0 -0
  257. /package/.server/server/{views → plugins/engine/views}/components/tag-env/template.njk +0 -0
  258. /package/.server/server/{views → plugins/engine/views}/confirmation.html +0 -0
  259. /package/.server/server/{views → plugins/engine/views}/layout.html +0 -0
  260. /package/.server/server/{views → plugins/engine/views}/summary.html +0 -0
@@ -0,0 +1,172 @@
1
+ import { ComponentType, type UkAddressFieldComponent } from '@defra/forms-model'
2
+ import { type ObjectSchema } from 'joi'
3
+
4
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
5
+ import {
6
+ FormComponent,
7
+ isFormState
8
+ } from '~/src/server/plugins/engine/components/FormComponent.js'
9
+ import { TextField } from '~/src/server/plugins/engine/components/TextField.js'
10
+ import { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
11
+ import {
12
+ type FormPayload,
13
+ type FormState,
14
+ type FormStateValue,
15
+ type FormSubmissionError,
16
+ type FormSubmissionState
17
+ } from '~/src/server/plugins/engine/types.js'
18
+
19
+ export class UkAddressField extends FormComponent {
20
+ declare options: UkAddressFieldComponent['options']
21
+ declare formSchema: ObjectSchema<FormPayload>
22
+ declare stateSchema: ObjectSchema<FormState>
23
+ declare collection: ComponentCollection
24
+
25
+ constructor(
26
+ def: UkAddressFieldComponent,
27
+ props: ConstructorParameters<typeof FormComponent>[1]
28
+ ) {
29
+ super(def, props)
30
+
31
+ const { name, options } = def
32
+
33
+ const isRequired = options.required !== false
34
+ const hideOptional = !!options.optionalText
35
+ const hideTitle = !!options.hideTitle
36
+
37
+ this.collection = new ComponentCollection(
38
+ [
39
+ {
40
+ type: ComponentType.TextField,
41
+ name: `${name}__addressLine1`,
42
+ title: 'Address line 1',
43
+ schema: { max: 100 },
44
+ options: {
45
+ autocomplete: 'address-line1',
46
+ required: isRequired,
47
+ optionalText: !isRequired && (hideOptional || !hideTitle)
48
+ }
49
+ },
50
+ {
51
+ type: ComponentType.TextField,
52
+ name: `${name}__addressLine2`,
53
+ title: 'Address line 2',
54
+ schema: { max: 100 },
55
+ options: {
56
+ autocomplete: 'address-line2',
57
+ required: false,
58
+ optionalText: !isRequired && (hideOptional || !hideTitle)
59
+ }
60
+ },
61
+ {
62
+ type: ComponentType.TextField,
63
+ name: `${name}__town`,
64
+ title: 'Town or city',
65
+ schema: { max: 100 },
66
+ options: {
67
+ autocomplete: 'address-level2',
68
+ classes: 'govuk-!-width-two-thirds',
69
+ required: isRequired,
70
+ optionalText: !isRequired && (hideOptional || !hideTitle)
71
+ }
72
+ },
73
+ {
74
+ type: ComponentType.TextField,
75
+ name: `${name}__postcode`,
76
+ title: 'Postcode',
77
+ schema: {
78
+ regex: '^[a-zA-Z]{1,2}\\d[a-zA-Z\\d]?\\s?\\d[a-zA-Z]{2}$'
79
+ },
80
+ options: {
81
+ autocomplete: 'postal-code',
82
+ classes: 'govuk-input--width-10',
83
+ required: isRequired,
84
+ optionalText: !isRequired && (hideOptional || !hideTitle)
85
+ }
86
+ }
87
+ ],
88
+ { ...props, parent: this }
89
+ )
90
+
91
+ this.options = options
92
+ this.formSchema = this.collection.formSchema
93
+ this.stateSchema = this.collection.stateSchema
94
+ }
95
+
96
+ getFormValueFromState(state: FormSubmissionState) {
97
+ const value = super.getFormValueFromState(state)
98
+ return this.isState(value) ? value : undefined
99
+ }
100
+
101
+ getDisplayStringFromState(state: FormSubmissionState) {
102
+ return this.getContextValueFromState(state)?.join(', ') ?? ''
103
+ }
104
+
105
+ getContextValueFromState(state: FormSubmissionState) {
106
+ const value = this.getFormValueFromState(state)
107
+
108
+ if (!value) {
109
+ return null
110
+ }
111
+
112
+ return Object.values(value).filter(Boolean)
113
+ }
114
+
115
+ getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
116
+ const { collection, name, options } = this
117
+
118
+ const viewModel = super.getViewModel(payload, errors)
119
+ let { components, fieldset, hint, label } = viewModel
120
+
121
+ fieldset ??= {
122
+ legend: {
123
+ text: label.text,
124
+
125
+ /**
126
+ * For screen readers, only hide legend visually. This can be overridden
127
+ * by single component {@link QuestionPageController | `showTitle` handling}
128
+ */
129
+ classes: options.hideTitle
130
+ ? 'govuk-visually-hidden'
131
+ : 'govuk-fieldset__legend--m'
132
+ }
133
+ }
134
+
135
+ if (hint) {
136
+ hint.id ??= `${name}-hint`
137
+ fieldset.attributes ??= {
138
+ 'aria-describedby': hint.id
139
+ }
140
+ }
141
+
142
+ components = collection.getViewModel(payload, errors)
143
+
144
+ return {
145
+ ...viewModel,
146
+ fieldset,
147
+ components
148
+ }
149
+ }
150
+
151
+ isState(value?: FormStateValue | FormState): value is UkAddressState {
152
+ return UkAddressField.isUkAddress(value)
153
+ }
154
+
155
+ static isUkAddress(
156
+ value?: FormStateValue | FormState
157
+ ): value is UkAddressState {
158
+ return (
159
+ isFormState(value) &&
160
+ TextField.isText(value.addressLine1) &&
161
+ TextField.isText(value.town) &&
162
+ TextField.isText(value.postcode)
163
+ )
164
+ }
165
+ }
166
+
167
+ export interface UkAddressState extends Record<string, string> {
168
+ addressLine1: string
169
+ addressLine2: string
170
+ town: string
171
+ postcode: string
172
+ }
@@ -0,0 +1,248 @@
1
+ import { ComponentType, type YesNoFieldComponent } from '@defra/forms-model'
2
+
3
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
4
+ import {
5
+ getAnswer,
6
+ type Field
7
+ } from '~/src/server/plugins/engine/components/helpers.js'
8
+ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
9
+ import { listYesNoExamples } from '~/test/fixtures/list.js'
10
+ import definition from '~/test/form/definitions/blank.js'
11
+ import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
12
+
13
+ describe('YesNoField', () => {
14
+ let def: YesNoFieldComponent
15
+ let model: FormModel
16
+ let collection: ComponentCollection
17
+ let field: Field
18
+
19
+ beforeEach(() => {
20
+ def = {
21
+ title: 'Example yes/no',
22
+ name: 'myComponent',
23
+ type: ComponentType.YesNoField,
24
+ options: {}
25
+ } satisfies YesNoFieldComponent
26
+
27
+ model = new FormModel(definition, {
28
+ basePath: 'test'
29
+ })
30
+
31
+ collection = new ComponentCollection([def], { model })
32
+ field = collection.fields[0]
33
+ })
34
+
35
+ describe('Schema', () => {
36
+ it('uses component title as label', () => {
37
+ const { formSchema } = collection
38
+ const { keys } = formSchema.describe()
39
+
40
+ expect(keys).toHaveProperty(
41
+ 'myComponent',
42
+ expect.objectContaining({
43
+ flags: expect.objectContaining({
44
+ label: 'Example yes/no'
45
+ })
46
+ })
47
+ )
48
+ })
49
+
50
+ it('uses component name as keys', () => {
51
+ const { formSchema } = collection
52
+ const { keys } = formSchema.describe()
53
+
54
+ expect(field.keys).toEqual(['myComponent'])
55
+ expect(field.collection).toBeUndefined()
56
+
57
+ for (const key of field.keys) {
58
+ expect(keys).toHaveProperty(key)
59
+ }
60
+ })
61
+
62
+ it('is required by default', () => {
63
+ const { formSchema } = collection
64
+ const { keys } = formSchema.describe()
65
+
66
+ expect(keys).toHaveProperty(
67
+ 'myComponent',
68
+ expect.objectContaining({
69
+ flags: expect.objectContaining({
70
+ presence: 'required'
71
+ })
72
+ })
73
+ )
74
+ })
75
+
76
+ it('is optional when configured', () => {
77
+ const collectionOptional = new ComponentCollection(
78
+ [{ ...def, options: { required: false } }],
79
+ { model }
80
+ )
81
+
82
+ const { formSchema } = collectionOptional
83
+ const { keys } = formSchema.describe()
84
+
85
+ expect(keys).toHaveProperty(
86
+ 'myComponent',
87
+ expect.objectContaining({
88
+ flags: expect.objectContaining({
89
+ presence: 'optional'
90
+ })
91
+ })
92
+ )
93
+
94
+ const result = collectionOptional.validate(getFormData())
95
+ expect(result.errors).toBeUndefined()
96
+ })
97
+
98
+ it('is configured with radio items', () => {
99
+ const { formSchema } = collection
100
+ const { keys } = formSchema.describe()
101
+
102
+ expect(keys).toHaveProperty(
103
+ 'myComponent',
104
+ expect.objectContaining({
105
+ allow: [true, false],
106
+ type: 'boolean'
107
+ })
108
+ )
109
+ })
110
+
111
+ it('accepts valid values', () => {
112
+ const result1 = collection.validate(getFormData('true'))
113
+ const result2 = collection.validate(getFormData('false'))
114
+
115
+ expect(result1.errors).toBeUndefined()
116
+ expect(result2.errors).toBeUndefined()
117
+ })
118
+
119
+ it('adds errors for empty value', () => {
120
+ const result = collection.validate(getFormData())
121
+
122
+ expect(result.errors).toEqual([
123
+ expect.objectContaining({
124
+ text: 'Select example yes/no'
125
+ })
126
+ ])
127
+ })
128
+
129
+ it('adds errors for invalid values', () => {
130
+ const result1 = collection.validate(getFormData('invalid'))
131
+ const result2 = collection.validate(getFormData(['true']))
132
+ const result3 = collection.validate(getFormData(['true', 'false']))
133
+
134
+ expect(result1.errors).toBeTruthy()
135
+ expect(result2.errors).toBeTruthy()
136
+ expect(result3.errors).toBeTruthy()
137
+ })
138
+ })
139
+
140
+ describe('State', () => {
141
+ it('returns text from state', () => {
142
+ const state1 = getFormState(true)
143
+ const state2 = getFormState(false)
144
+ const state3 = getFormState(null)
145
+
146
+ const answer1 = getAnswer(field, state1)
147
+ const answer2 = getAnswer(field, state2)
148
+ const answer3 = getAnswer(field, state3)
149
+
150
+ expect(answer1).toBe('Yes')
151
+ expect(answer2).toBe('No')
152
+ expect(answer3).toBe('')
153
+ })
154
+
155
+ it('returns payload from state', () => {
156
+ const state1 = getFormState(true)
157
+ const state2 = getFormState(false)
158
+ const state3 = getFormState(null)
159
+
160
+ const payload1 = field.getFormDataFromState(state1)
161
+ const payload2 = field.getFormDataFromState(state2)
162
+ const payload3 = field.getFormDataFromState(state3)
163
+
164
+ expect(payload1).toEqual(getFormData(true))
165
+ expect(payload2).toEqual(getFormData(false))
166
+ expect(payload3).toEqual(getFormData())
167
+ })
168
+
169
+ it('returns value from state', () => {
170
+ const state1 = getFormState(true)
171
+ const state2 = getFormState(false)
172
+ const state3 = getFormState(null)
173
+
174
+ const value1 = field.getFormValueFromState(state1)
175
+ const value2 = field.getFormValueFromState(state2)
176
+ const value3 = field.getFormValueFromState(state3)
177
+
178
+ expect(value1).toBe(true)
179
+ expect(value2).toBe(false)
180
+ expect(value3).toBeUndefined()
181
+ })
182
+
183
+ it('returns context for conditions and form submission', () => {
184
+ const state1 = getFormState(true)
185
+ const state2 = getFormState(false)
186
+ const state3 = getFormState(null)
187
+
188
+ const value1 = field.getContextValueFromState(state1)
189
+ const value2 = field.getContextValueFromState(state2)
190
+ const value3 = field.getContextValueFromState(state3)
191
+
192
+ expect(value1).toBe(true)
193
+ expect(value2).toBe(false)
194
+ expect(value3).toBeNull()
195
+ })
196
+
197
+ it('returns state from payload', () => {
198
+ const payload1 = getFormData(true)
199
+ const payload2 = getFormData(false)
200
+ const payload3 = getFormData()
201
+
202
+ const value1 = field.getStateFromValidForm(payload1)
203
+ const value2 = field.getStateFromValidForm(payload2)
204
+ const value3 = field.getStateFromValidForm(payload3)
205
+
206
+ expect(value1).toEqual(getFormState(true))
207
+ expect(value2).toEqual(getFormState(false))
208
+ expect(value3).toEqual(getFormState(null))
209
+ })
210
+ })
211
+
212
+ describe('View model', () => {
213
+ const items = listYesNoExamples
214
+
215
+ it('sets Nunjucks component defaults', () => {
216
+ const item = items[0]
217
+
218
+ const viewModel = field.getViewModel(getFormData(item.value))
219
+
220
+ expect(viewModel).toEqual(
221
+ expect.objectContaining({
222
+ label: { text: def.title },
223
+ name: 'myComponent',
224
+ id: 'myComponent',
225
+ value: item.value
226
+ })
227
+ )
228
+ })
229
+
230
+ it.each([...items])('sets Nunjucks component radio items', (item) => {
231
+ const viewModel = field.getViewModel(getFormData(item.value))
232
+
233
+ expect(viewModel.items?.[0]).not.toMatchObject({
234
+ value: '' // First item is never empty
235
+ })
236
+
237
+ expect(viewModel.items).toEqual(
238
+ expect.arrayContaining([
239
+ expect.objectContaining({
240
+ text: item.text,
241
+ value: item.value,
242
+ checked: true
243
+ })
244
+ ])
245
+ )
246
+ })
247
+ })
248
+ })
@@ -0,0 +1,31 @@
1
+ import { type YesNoFieldComponent } from '@defra/forms-model'
2
+
3
+ import { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js'
4
+ import { addClassOptionIfNone } from '~/src/server/plugins/engine/components/helpers.js'
5
+
6
+ /**
7
+ * @description
8
+ * YesNoField is a radiosField with predefined values.
9
+ */
10
+ export class YesNoField extends SelectionControlField {
11
+ declare options: YesNoFieldComponent['options']
12
+
13
+ constructor(
14
+ def: YesNoFieldComponent,
15
+ props: ConstructorParameters<typeof SelectionControlField>[1]
16
+ ) {
17
+ super({ ...def, list: '__yesNo' }, props)
18
+
19
+ const { options } = def
20
+ let { formSchema } = this
21
+
22
+ addClassOptionIfNone(options, 'govuk-radios--inline')
23
+
24
+ if (options.required === false) {
25
+ formSchema = formSchema.optional()
26
+ }
27
+
28
+ this.formSchema = formSchema
29
+ this.options = options
30
+ }
31
+ }
@@ -0,0 +1 @@
1
+ export const optionalText = ' (optional)'