@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,163 @@
1
+ import { type NumberFieldComponent } from '@defra/forms-model'
2
+ import joi, { type CustomValidator, type NumberSchema } from 'joi'
3
+
4
+ import {
5
+ FormComponent,
6
+ isFormValue
7
+ } from '~/src/server/plugins/engine/components/FormComponent.js'
8
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
9
+ import {
10
+ type FormPayload,
11
+ type FormState,
12
+ type FormStateValue,
13
+ type FormSubmissionError,
14
+ type FormSubmissionState
15
+ } from '~/src/server/plugins/engine/types.js'
16
+
17
+ export class NumberField extends FormComponent {
18
+ declare options: NumberFieldComponent['options']
19
+ declare schema: NumberFieldComponent['schema']
20
+ declare formSchema: NumberSchema
21
+ declare stateSchema: NumberSchema
22
+
23
+ constructor(
24
+ def: NumberFieldComponent,
25
+ props: ConstructorParameters<typeof FormComponent>[1]
26
+ ) {
27
+ super(def, props)
28
+
29
+ const { options, schema, title } = def
30
+
31
+ let formSchema = joi
32
+ .number()
33
+ .custom(getValidatorPrecision(this))
34
+ .label(title)
35
+ .required()
36
+
37
+ if (options.required === false) {
38
+ formSchema = formSchema.allow('')
39
+ } else {
40
+ const messages = options.customValidationMessages
41
+
42
+ formSchema = formSchema.empty('').messages({
43
+ 'any.required': messages?.['any.required'] ?? messageTemplate.required
44
+ })
45
+ }
46
+
47
+ if (typeof schema.min === 'number') {
48
+ formSchema = formSchema.min(schema.min)
49
+ }
50
+
51
+ if (typeof schema.max === 'number') {
52
+ formSchema = formSchema.max(schema.max)
53
+ }
54
+
55
+ if (typeof schema.precision === 'number' && schema.precision <= 0) {
56
+ formSchema = formSchema.integer()
57
+ }
58
+
59
+ if (options.customValidationMessage) {
60
+ const message = options.customValidationMessage
61
+
62
+ formSchema = formSchema.messages({
63
+ 'any.required': message,
64
+ 'number.base': message,
65
+ 'number.precision': message,
66
+ 'number.integer': message,
67
+ 'number.min': message,
68
+ 'number.max': message
69
+ })
70
+ } else if (options.customValidationMessages) {
71
+ formSchema = formSchema.messages(options.customValidationMessages)
72
+ }
73
+
74
+ this.formSchema = formSchema.default('')
75
+ this.stateSchema = formSchema.default(null).allow(null)
76
+ this.options = options
77
+ this.schema = schema
78
+ }
79
+
80
+ getFormValueFromState(state: FormSubmissionState) {
81
+ const { name } = this
82
+ return this.getFormValue(state[name])
83
+ }
84
+
85
+ getFormValue(value?: FormStateValue | FormState) {
86
+ return this.isValue(value) ? value : undefined
87
+ }
88
+
89
+ getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
90
+ const { options, schema } = this
91
+
92
+ const viewModel = super.getViewModel(payload, errors)
93
+ let { attributes, prefix, suffix, value } = viewModel
94
+
95
+ if (typeof schema.precision === 'undefined' || schema.precision <= 0) {
96
+ // If precision isn't provided or provided and
97
+ // less than or equal to 0, use numeric inputmode
98
+ attributes.inputmode = 'numeric'
99
+ }
100
+
101
+ if (options.prefix) {
102
+ prefix = {
103
+ text: options.prefix
104
+ }
105
+ }
106
+
107
+ if (options.suffix) {
108
+ suffix = {
109
+ text: options.suffix
110
+ }
111
+ }
112
+
113
+ // Allow any `toString()`-able value so non-numeric
114
+ // values are shown alongside their error messages
115
+ if (!isFormValue(value)) {
116
+ value = undefined
117
+ }
118
+
119
+ return {
120
+ ...viewModel,
121
+ attributes,
122
+ prefix,
123
+ suffix,
124
+ value
125
+ }
126
+ }
127
+
128
+ isValue(value?: FormStateValue | FormState) {
129
+ return NumberField.isNumber(value)
130
+ }
131
+
132
+ static isNumber(value?: FormStateValue | FormState): value is number {
133
+ return typeof value === 'number'
134
+ }
135
+ }
136
+
137
+ export function getValidatorPrecision(component: NumberField) {
138
+ const validator: CustomValidator = (value: number, helpers) => {
139
+ const { options, schema } = component
140
+
141
+ const { customValidationMessage: custom } = options
142
+ const { precision: limit } = schema
143
+
144
+ if (!limit || limit <= 0) {
145
+ return value
146
+ }
147
+
148
+ const validationSchema = joi
149
+ .number()
150
+ .precision(limit)
151
+ .prefs({ convert: false })
152
+
153
+ try {
154
+ return joi.attempt(value, validationSchema)
155
+ } catch {
156
+ return custom
157
+ ? helpers.message({ custom }, { limit })
158
+ : helpers.error('number.precision', { limit })
159
+ }
160
+ }
161
+
162
+ return validator
163
+ }
@@ -0,0 +1,288 @@
1
+ import { ComponentType, type RadiosFieldComponent } from '@defra/forms-model'
2
+
3
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
4
+ import { RadiosField } from '~/src/server/plugins/engine/components/RadiosField.js'
5
+ import {
6
+ getAnswer,
7
+ type Field
8
+ } from '~/src/server/plugins/engine/components/helpers.js'
9
+ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
10
+ import {
11
+ listNumber,
12
+ listNumberExamples,
13
+ listString,
14
+ listStringExamples
15
+ } from '~/test/fixtures/list.js'
16
+ import definition from '~/test/form/definitions/blank.js'
17
+ import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
18
+
19
+ describe.each([
20
+ {
21
+ component: {
22
+ title: 'String list',
23
+ name: 'myComponent',
24
+ type: ComponentType.RadiosField,
25
+ list: 'listString',
26
+ options: {}
27
+ } satisfies RadiosFieldComponent,
28
+
29
+ options: {
30
+ list: listString,
31
+ examples: listStringExamples,
32
+ allow: ['1', '2', '3', '4']
33
+ }
34
+ },
35
+ {
36
+ component: {
37
+ title: 'Number list',
38
+ name: 'myComponent',
39
+ type: ComponentType.RadiosField,
40
+ list: 'listNumber',
41
+ options: {}
42
+ } satisfies RadiosFieldComponent,
43
+
44
+ options: {
45
+ list: listNumber,
46
+ examples: listNumberExamples,
47
+ allow: [1, 2, 3, 4]
48
+ }
49
+ }
50
+ ])('RadiosField: $component.title', ({ component: def, options }) => {
51
+ const updated = structuredClone(definition)
52
+ updated.lists = [options.list]
53
+
54
+ let model: FormModel
55
+ let collection: ComponentCollection
56
+ let field: Field
57
+
58
+ beforeEach(() => {
59
+ model = new FormModel(updated, {
60
+ basePath: 'test'
61
+ })
62
+
63
+ collection = new ComponentCollection([def], { model })
64
+ field = collection.fields[0]
65
+ })
66
+
67
+ describe('Defaults', () => {
68
+ describe('Schema', () => {
69
+ it('uses component title as label', () => {
70
+ const { formSchema } = collection
71
+ const { keys } = formSchema.describe()
72
+
73
+ expect(keys).toHaveProperty(
74
+ 'myComponent',
75
+ expect.objectContaining({
76
+ flags: expect.objectContaining({
77
+ label: def.title
78
+ })
79
+ })
80
+ )
81
+ })
82
+
83
+ it('uses component name as keys', () => {
84
+ const { formSchema } = collection
85
+ const { keys } = formSchema.describe()
86
+
87
+ expect(field.keys).toEqual(['myComponent'])
88
+ expect(field.collection).toBeUndefined()
89
+
90
+ for (const key of field.keys) {
91
+ expect(keys).toHaveProperty(key)
92
+ }
93
+ })
94
+
95
+ it('is required by default', () => {
96
+ const { formSchema } = collection
97
+ const { keys } = formSchema.describe()
98
+
99
+ expect(keys).toHaveProperty(
100
+ 'myComponent',
101
+ expect.objectContaining({
102
+ flags: expect.objectContaining({
103
+ presence: 'required'
104
+ })
105
+ })
106
+ )
107
+ })
108
+
109
+ it('is optional when configured', () => {
110
+ const collectionOptional = new ComponentCollection(
111
+ [{ ...def, options: { required: false } }],
112
+ { model }
113
+ )
114
+
115
+ const { formSchema } = collectionOptional
116
+ const { keys } = formSchema.describe()
117
+
118
+ expect(keys).toHaveProperty(
119
+ 'myComponent',
120
+ expect.objectContaining({
121
+ flags: expect.objectContaining({
122
+ presence: 'optional'
123
+ })
124
+ })
125
+ )
126
+
127
+ const result = collectionOptional.validate(getFormData())
128
+ expect(result.errors).toBeUndefined()
129
+ })
130
+
131
+ it('is configured with radio items', () => {
132
+ const { formSchema } = collection
133
+ const { keys } = formSchema.describe()
134
+
135
+ expect(keys).toHaveProperty(
136
+ 'myComponent',
137
+ expect.objectContaining({
138
+ allow: options.allow,
139
+ type: options.list.type
140
+ })
141
+ )
142
+ })
143
+
144
+ it.each([...options.allow])('accepts valid radio item', (value) => {
145
+ const result = collection.validate(getFormData(value))
146
+ expect(result.errors).toBeUndefined()
147
+ })
148
+
149
+ it('adds errors for empty value', () => {
150
+ const result = collection.validate(getFormData())
151
+
152
+ expect(result.errors).toEqual([
153
+ expect.objectContaining({
154
+ text: `Select ${def.title.toLowerCase()}`
155
+ })
156
+ ])
157
+ })
158
+
159
+ it('adds errors for invalid values', () => {
160
+ const result1 = collection.validate(getFormData('invalid'))
161
+ const result2 = collection.validate(
162
+ // @ts-expect-error - Allow invalid param for test
163
+ getFormData({ unknown: 'invalid' })
164
+ )
165
+
166
+ expect(result1.errors).toBeTruthy()
167
+ expect(result2.errors).toBeTruthy()
168
+ })
169
+ })
170
+
171
+ describe('State', () => {
172
+ it.each([...options.examples])('returns text from state', (item) => {
173
+ const state1 = getFormState(item.state)
174
+ const state2 = getFormState(null)
175
+
176
+ const answer1 = getAnswer(field, state1)
177
+ const answer2 = getAnswer(field, state2)
178
+
179
+ expect(answer1).toBe(item.text)
180
+ expect(answer2).toBe('')
181
+ })
182
+
183
+ it.each([...options.examples])('returns payload from state', (item) => {
184
+ const state1 = getFormState(item.state)
185
+ const state2 = getFormState(null)
186
+
187
+ const payload1 = field.getFormDataFromState(state1)
188
+ const payload2 = field.getFormDataFromState(state2)
189
+
190
+ expect(payload1).toEqual(getFormData(item.value))
191
+ expect(payload2).toEqual(getFormData())
192
+ })
193
+
194
+ it.each([...options.examples])('returns value from state', (item) => {
195
+ const state1 = getFormState(item.state)
196
+ const state2 = getFormState(null)
197
+
198
+ const value1 = field.getFormValueFromState(state1)
199
+ const value2 = field.getFormValueFromState(state2)
200
+
201
+ expect(value1).toEqual(item.value)
202
+ expect(value2).toBeUndefined()
203
+ })
204
+
205
+ it.each([...options.examples])(
206
+ 'returns context for conditions and form submission',
207
+ (item) => {
208
+ const state1 = getFormState(item.state)
209
+ const state2 = getFormState(null)
210
+
211
+ const value1 = field.getContextValueFromState(state1)
212
+ const value2 = field.getContextValueFromState(state2)
213
+
214
+ expect(value1).toEqual(item.value)
215
+ expect(value2).toBeNull()
216
+ }
217
+ )
218
+
219
+ it.each([...options.examples])('returns state from payload', (item) => {
220
+ const payload1 = getFormData(item.value)
221
+ const payload2 = getFormData()
222
+
223
+ const value1 = field.getStateFromValidForm(payload1)
224
+ const value2 = field.getStateFromValidForm(payload2)
225
+
226
+ expect(value1).toEqual(getFormState(item.state))
227
+ expect(value2).toEqual(getFormState(null))
228
+ })
229
+ })
230
+
231
+ describe('View model', () => {
232
+ it('sets Nunjucks component defaults', () => {
233
+ const item = options.examples[0]
234
+
235
+ const viewModel = field.getViewModel(getFormData(item.value))
236
+
237
+ expect(viewModel).toEqual(
238
+ expect.objectContaining({
239
+ label: { text: def.title },
240
+ name: 'myComponent',
241
+ id: 'myComponent',
242
+ value: item.value
243
+ })
244
+ )
245
+ })
246
+
247
+ it.each([...options.examples])(
248
+ 'sets Nunjucks component radio items',
249
+ (item) => {
250
+ const viewModel = field.getViewModel(getFormData(item.value))
251
+
252
+ expect(viewModel.items?.[0]).not.toMatchObject({
253
+ value: '' // First item is never empty
254
+ })
255
+
256
+ expect(viewModel.items).toEqual(
257
+ expect.arrayContaining([
258
+ expect.objectContaining({
259
+ text: item.text,
260
+ value: item.value,
261
+ checked: true
262
+ })
263
+ ])
264
+ )
265
+ }
266
+ )
267
+ })
268
+
269
+ describe('Radio items', () => {
270
+ it('returns radio items', () => {
271
+ expect(field).toHaveProperty('items', options.list.items)
272
+ })
273
+
274
+ it('returns radio items matching type', () => {
275
+ expect(field).toHaveProperty('values', expect.arrayContaining([]))
276
+ })
277
+
278
+ it('returns empty items when missing', () => {
279
+ const model = new FormModel(definition, {
280
+ basePath: 'test'
281
+ })
282
+
283
+ const { items } = new RadiosField(def, { model })
284
+ expect(items).toEqual([])
285
+ })
286
+ })
287
+ })
288
+ })
@@ -0,0 +1,24 @@
1
+ import { type RadiosFieldComponent } from '@defra/forms-model'
2
+
3
+ import { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js'
4
+
5
+ export class RadiosField extends SelectionControlField {
6
+ declare options: RadiosFieldComponent['options']
7
+
8
+ constructor(
9
+ def: RadiosFieldComponent,
10
+ props: ConstructorParameters<typeof SelectionControlField>[1]
11
+ ) {
12
+ super(def, props)
13
+
14
+ const { options } = def
15
+ let { formSchema } = this
16
+
17
+ if (options.required === false) {
18
+ formSchema = formSchema.optional()
19
+ }
20
+
21
+ this.formSchema = formSchema
22
+ this.options = options
23
+ }
24
+ }