@defra/forms-engine-plugin 0.1.11 → 0.1.13

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 (147) hide show
  1. package/.public/javascripts/file-upload.min.js +1 -1
  2. package/.public/javascripts/file-upload.min.js.map +1 -1
  3. package/.server/client/javascripts/file-upload.js +45 -4
  4. package/.server/client/javascripts/file-upload.js.map +1 -1
  5. package/.server/server/constants.js +2 -0
  6. package/.server/server/constants.js.map +1 -1
  7. package/.server/server/index.js +1 -1
  8. package/.server/server/index.js.map +1 -1
  9. package/.server/server/plugins/engine/components/AutocompleteField.js +2 -0
  10. package/.server/server/plugins/engine/components/AutocompleteField.js.map +1 -1
  11. package/.server/server/plugins/engine/components/CheckboxesField.js +3 -4
  12. package/.server/server/plugins/engine/components/CheckboxesField.js.map +1 -1
  13. package/.server/server/plugins/engine/components/ComponentCollection.js +37 -16
  14. package/.server/server/plugins/engine/components/ComponentCollection.js.map +1 -1
  15. package/.server/server/plugins/engine/components/DatePartsField.js +36 -2
  16. package/.server/server/plugins/engine/components/DatePartsField.js.map +1 -1
  17. package/.server/server/plugins/engine/components/EmailAddressField.js +19 -3
  18. package/.server/server/plugins/engine/components/EmailAddressField.js.map +1 -1
  19. package/.server/server/plugins/engine/components/FileUploadField.js +44 -4
  20. package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
  21. package/.server/server/plugins/engine/components/FormComponent.js +14 -2
  22. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  23. package/.server/server/plugins/engine/components/ListFormComponent.js +16 -3
  24. package/.server/server/plugins/engine/components/ListFormComponent.js.map +1 -1
  25. package/.server/server/plugins/engine/components/Markdown.js +24 -0
  26. package/.server/server/plugins/engine/components/Markdown.js.map +1 -0
  27. package/.server/server/plugins/engine/components/MonthYearField.js +30 -2
  28. package/.server/server/plugins/engine/components/MonthYearField.js.map +1 -1
  29. package/.server/server/plugins/engine/components/MultilineTextField.js +32 -3
  30. package/.server/server/plugins/engine/components/MultilineTextField.js.map +1 -1
  31. package/.server/server/plugins/engine/components/NumberField.js +28 -3
  32. package/.server/server/plugins/engine/components/NumberField.js.map +1 -1
  33. package/.server/server/plugins/engine/components/SelectionControlField.js +14 -0
  34. package/.server/server/plugins/engine/components/SelectionControlField.js.map +1 -1
  35. package/.server/server/plugins/engine/components/TelephoneNumberField.js +19 -3
  36. package/.server/server/plugins/engine/components/TelephoneNumberField.js.map +1 -1
  37. package/.server/server/plugins/engine/components/TextField.js +22 -3
  38. package/.server/server/plugins/engine/components/TextField.js.map +1 -1
  39. package/.server/server/plugins/engine/components/UkAddressField.js +29 -0
  40. package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
  41. package/.server/server/plugins/engine/components/YesNoField.js +18 -0
  42. package/.server/server/plugins/engine/components/YesNoField.js.map +1 -1
  43. package/.server/server/plugins/engine/components/helpers.js +16 -0
  44. package/.server/server/plugins/engine/components/helpers.js.map +1 -1
  45. package/.server/server/plugins/engine/components/index.js +1 -0
  46. package/.server/server/plugins/engine/components/index.js.map +1 -1
  47. package/.server/server/plugins/engine/configureEnginePlugin.js +3 -1
  48. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  49. package/.server/server/plugins/engine/helpers.js +38 -18
  50. package/.server/server/plugins/engine/helpers.js.map +1 -1
  51. package/.server/server/plugins/engine/models/FormModel.js +60 -2
  52. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  53. package/.server/server/plugins/engine/models/SummaryViewModel.js +3 -2
  54. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  55. package/.server/server/plugins/engine/outputFormatters/human/v1.js +1 -1
  56. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  57. package/.server/server/plugins/engine/pageControllers/PageController.js +13 -5
  58. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  59. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +2 -2
  60. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  61. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +19 -5
  62. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  63. package/.server/server/plugins/engine/pageControllers/validationOptions.js +6 -11
  64. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  65. package/.server/server/plugins/engine/plugin.js +5 -4
  66. package/.server/server/plugins/engine/plugin.js.map +1 -1
  67. package/.server/server/plugins/engine/services/notifyService.js +1 -4
  68. package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
  69. package/.server/server/plugins/engine/services/uploadService.js +5 -3
  70. package/.server/server/plugins/engine/services/uploadService.js.map +1 -1
  71. package/.server/server/plugins/engine/types.js.map +1 -1
  72. package/.server/server/plugins/engine/views/components/html.html +1 -1
  73. package/.server/server/plugins/engine/views/components/markdown.html +5 -0
  74. package/.server/server/plugins/engine/views/summary.html +7 -1
  75. package/.server/server/plugins/nunjucks/context.js +7 -6
  76. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  77. package/.server/server/plugins/nunjucks/enviroment.test.js +6 -3
  78. package/.server/server/plugins/nunjucks/enviroment.test.js.map +1 -1
  79. package/.server/server/utils/type-utils.js +8 -0
  80. package/.server/server/utils/type-utils.js.map +1 -0
  81. package/.server/typings/joi/index.d.js.map +1 -1
  82. package/package.json +3 -3
  83. package/src/client/javascripts/file-upload.js +60 -4
  84. package/src/server/constants.js +2 -0
  85. package/src/server/index.test.ts +34 -29
  86. package/src/server/index.ts +2 -1
  87. package/src/server/plugins/engine/components/AutocompleteField.test.ts +71 -3
  88. package/src/server/plugins/engine/components/AutocompleteField.ts +6 -2
  89. package/src/server/plugins/engine/components/CheckboxesField.test.ts +40 -8
  90. package/src/server/plugins/engine/components/CheckboxesField.ts +7 -3
  91. package/src/server/plugins/engine/components/ComponentCollection.ts +45 -18
  92. package/src/server/plugins/engine/components/DatePartsField.test.ts +13 -4
  93. package/src/server/plugins/engine/components/DatePartsField.ts +29 -8
  94. package/src/server/plugins/engine/components/EmailAddressField.test.ts +51 -1
  95. package/src/server/plugins/engine/components/EmailAddressField.ts +17 -2
  96. package/src/server/plugins/engine/components/FileUploadField.test.ts +53 -0
  97. package/src/server/plugins/engine/components/FileUploadField.ts +52 -3
  98. package/src/server/plugins/engine/components/FormComponent.ts +24 -2
  99. package/src/server/plugins/engine/components/ListFormComponent.ts +16 -2
  100. package/src/server/plugins/engine/components/Markdown.test.ts +48 -0
  101. package/src/server/plugins/engine/components/Markdown.ts +29 -0
  102. package/src/server/plugins/engine/components/MonthYearField.test.ts +35 -0
  103. package/src/server/plugins/engine/components/MonthYearField.ts +34 -9
  104. package/src/server/plugins/engine/components/MultilineTextField.test.ts +83 -5
  105. package/src/server/plugins/engine/components/MultilineTextField.ts +37 -2
  106. package/src/server/plugins/engine/components/NumberField.test.ts +24 -2
  107. package/src/server/plugins/engine/components/NumberField.ts +23 -3
  108. package/src/server/plugins/engine/components/RadiosField.test.ts +10 -1
  109. package/src/server/plugins/engine/components/SelectField.test.ts +2 -1
  110. package/src/server/plugins/engine/components/SelectionControlField.ts +14 -0
  111. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +30 -2
  112. package/src/server/plugins/engine/components/TelephoneNumberField.ts +17 -2
  113. package/src/server/plugins/engine/components/TextField.test.ts +33 -1
  114. package/src/server/plugins/engine/components/TextField.ts +17 -2
  115. package/src/server/plugins/engine/components/UkAddressField.test.ts +46 -3
  116. package/src/server/plugins/engine/components/UkAddressField.ts +28 -0
  117. package/src/server/plugins/engine/components/YesNoField.test.ts +9 -1
  118. package/src/server/plugins/engine/components/YesNoField.ts +24 -0
  119. package/src/server/plugins/engine/components/helpers.test.ts +24 -0
  120. package/src/server/plugins/engine/components/helpers.ts +39 -0
  121. package/src/server/plugins/engine/components/index.ts +1 -0
  122. package/src/server/plugins/engine/configureEnginePlugin.ts +13 -3
  123. package/src/server/plugins/engine/helpers.test.ts +71 -20
  124. package/src/server/plugins/engine/helpers.ts +46 -19
  125. package/src/server/plugins/engine/models/FormModel.test.ts +91 -1
  126. package/src/server/plugins/engine/models/FormModel.ts +86 -3
  127. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +46 -7
  128. package/src/server/plugins/engine/models/SummaryViewModel.ts +7 -3
  129. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +1 -2
  130. package/src/server/plugins/engine/outputFormatters/human/v1.ts +1 -1
  131. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1 -0
  132. package/src/server/plugins/engine/pageControllers/PageController.test.ts +9 -6
  133. package/src/server/plugins/engine/pageControllers/PageController.ts +15 -5
  134. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +2 -2
  135. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +21 -6
  136. package/src/server/plugins/engine/pageControllers/validationOptions.ts +31 -17
  137. package/src/server/plugins/engine/plugin.ts +9 -5
  138. package/src/server/plugins/engine/services/notifyService.ts +1 -2
  139. package/src/server/plugins/engine/services/uploadService.js +10 -6
  140. package/src/server/plugins/engine/types.ts +10 -1
  141. package/src/server/plugins/engine/views/components/html.html +1 -1
  142. package/src/server/plugins/engine/views/components/markdown.html +5 -0
  143. package/src/server/plugins/engine/views/summary.html +7 -1
  144. package/src/server/plugins/nunjucks/context.js +5 -5
  145. package/src/server/plugins/nunjucks/enviroment.test.js +9 -3
  146. package/src/server/utils/type-utils.ts +15 -0
  147. package/src/typings/joi/index.d.ts +8 -0
@@ -7,6 +7,7 @@ import {
7
7
  } from '~/src/server/plugins/engine/components/FormComponent.js'
8
8
  import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
9
9
  import {
10
+ type ErrorMessageTemplateList,
10
11
  type FormPayload,
11
12
  type FormState,
12
13
  type FormStateValue,
@@ -26,12 +27,12 @@ export class NumberField extends FormComponent {
26
27
  ) {
27
28
  super(def, props)
28
29
 
29
- const { options, schema, title } = def
30
+ const { options, schema } = def
30
31
 
31
32
  let formSchema = joi
32
33
  .number()
33
34
  .custom(getValidatorPrecision(this))
34
- .label(title)
35
+ .label(this.label)
35
36
  .required()
36
37
 
37
38
  if (options.required === false) {
@@ -40,7 +41,9 @@ export class NumberField extends FormComponent {
40
41
  const messages = options.customValidationMessages
41
42
 
42
43
  formSchema = formSchema.empty('').messages({
43
- 'any.required': messages?.['any.required'] ?? messageTemplate.required
44
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
45
+ 'any.required':
46
+ messages?.['any.required'] ?? (messageTemplate.required as string)
44
47
  })
45
48
  }
46
49
 
@@ -129,6 +132,23 @@ export class NumberField extends FormComponent {
129
132
  return NumberField.isNumber(value)
130
133
  }
131
134
 
135
+ /**
136
+ * For error preview page that shows all possible errors on a component
137
+ */
138
+ getAllPossibleErrors(): ErrorMessageTemplateList {
139
+ return {
140
+ baseErrors: [
141
+ { type: 'required', template: messageTemplate.required },
142
+ { type: 'numberInteger', template: messageTemplate.numberInteger }
143
+ ],
144
+ advancedSettingsErrors: [
145
+ { type: 'numberMin', template: messageTemplate.numberMin },
146
+ { type: 'numberMax', template: messageTemplate.numberMax },
147
+ { type: 'numberPrecision', template: messageTemplate.numberPrecision }
148
+ ]
149
+ }
150
+ }
151
+
132
152
  static isNumber(value?: FormStateValue | FormState): value is number {
133
153
  return typeof value === 'number'
134
154
  }
@@ -1,4 +1,5 @@
1
1
  import { ComponentType, type RadiosFieldComponent } from '@defra/forms-model'
2
+ import lowerFirst from 'lodash/lowerFirst.js'
2
3
 
3
4
  import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
4
5
  import { RadiosField } from '~/src/server/plugins/engine/components/RadiosField.js'
@@ -151,7 +152,7 @@ describe.each([
151
152
 
152
153
  expect(result.errors).toEqual([
153
154
  expect.objectContaining({
154
- text: `Select ${def.title.toLowerCase()}`
155
+ text: `Select ${lowerFirst(def.title)}`
155
156
  })
156
157
  ])
157
158
  })
@@ -284,5 +285,13 @@ describe.each([
284
285
  expect(items).toEqual([])
285
286
  })
286
287
  })
288
+
289
+ describe('AllPossibleErrors', () => {
290
+ it('should return errors', () => {
291
+ const errors = field.getAllPossibleErrors()
292
+ expect(errors.baseErrors).not.toBeEmpty()
293
+ expect(errors.advancedSettingsErrors).toBeEmpty()
294
+ })
295
+ })
287
296
  })
288
297
  })
@@ -1,4 +1,5 @@
1
1
  import { ComponentType, type SelectFieldComponent } from '@defra/forms-model'
2
+ import lowerFirst from 'lodash/lowerFirst.js'
2
3
 
3
4
  import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
4
5
  import { SelectField } from '~/src/server/plugins/engine/components/SelectField.js'
@@ -152,7 +153,7 @@ describe.each([
152
153
 
153
154
  expect(result.errors).toEqual([
154
155
  expect.objectContaining({
155
- text: `Select ${def.title.toLowerCase()}`
156
+ text: `Select ${lowerFirst(def.title)}`
156
157
  })
157
158
  ])
158
159
  })
@@ -1,6 +1,8 @@
1
1
  import { ListFormComponent } from '~/src/server/plugins/engine/components/ListFormComponent.js'
2
2
  import { type ListItem } from '~/src/server/plugins/engine/components/types.js'
3
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
3
4
  import {
5
+ type ErrorMessageTemplateList,
4
6
  type FormPayload,
5
7
  type FormSubmissionError
6
8
  } from '~/src/server/plugins/engine/types.js'
@@ -40,4 +42,16 @@ export class SelectionControlField extends ListFormComponent {
40
42
  items
41
43
  }
42
44
  }
45
+
46
+ /**
47
+ * For error preview page that shows all possible errors on a component
48
+ */
49
+ getAllPossibleErrors(): ErrorMessageTemplateList {
50
+ return {
51
+ baseErrors: [
52
+ { type: 'selectRequired', template: messageTemplate.selectRequired }
53
+ ],
54
+ advancedSettingsErrors: []
55
+ }
56
+ }
43
57
  }
@@ -29,6 +29,7 @@ describe('TelephoneNumberField', () => {
29
29
  beforeEach(() => {
30
30
  def = {
31
31
  title: 'Example telephone number field',
32
+ shortDescription: 'Example telephone number',
32
33
  name: 'myComponent',
33
34
  type: ComponentType.TelephoneNumberField,
34
35
  options: {}
@@ -39,7 +40,7 @@ describe('TelephoneNumberField', () => {
39
40
  })
40
41
 
41
42
  describe('Schema', () => {
42
- it('uses component title as label', () => {
43
+ it('uses component short description as label', () => {
43
44
  const { formSchema } = collection
44
45
  const { keys } = formSchema.describe()
45
46
 
@@ -47,7 +48,7 @@ describe('TelephoneNumberField', () => {
47
48
  'myComponent',
48
49
  expect.objectContaining({
49
50
  flags: expect.objectContaining({
50
- label: 'Example telephone number field'
51
+ label: 'Example telephone number'
51
52
  })
52
53
  })
53
54
  )
@@ -121,6 +122,25 @@ describe('TelephoneNumberField', () => {
121
122
  it('adds errors for empty value', () => {
122
123
  const result = collection.validate(getFormData(''))
123
124
 
125
+ expect(result.errors).toEqual([
126
+ expect.objectContaining({
127
+ text: 'Enter example telephone number'
128
+ })
129
+ ])
130
+ })
131
+
132
+ it('adds errors for empty value given no short description exists', () => {
133
+ def = {
134
+ title: 'Example telephone number field',
135
+ name: 'myComponent',
136
+ type: ComponentType.TelephoneNumberField,
137
+ options: {}
138
+ } satisfies TelephoneNumberFieldComponent
139
+
140
+ collection = new ComponentCollection([def], { model })
141
+
142
+ const result = collection.validate(getFormData(''))
143
+
124
144
  expect(result.errors).toEqual([
125
145
  expect.objectContaining({
126
146
  text: 'Enter example telephone number field'
@@ -215,6 +235,14 @@ describe('TelephoneNumberField', () => {
215
235
  )
216
236
  })
217
237
  })
238
+
239
+ describe('AllPossibleErrors', () => {
240
+ it('should return errors', () => {
241
+ const errors = field.getAllPossibleErrors()
242
+ expect(errors.baseErrors).not.toBeEmpty()
243
+ expect(errors.advancedSettingsErrors).toBeEmpty()
244
+ })
245
+ })
218
246
  })
219
247
 
220
248
  describe('Validation', () => {
@@ -3,7 +3,9 @@ import joi, { type StringSchema } from 'joi'
3
3
 
4
4
  import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
5
5
  import { addClassOptionIfNone } from '~/src/server/plugins/engine/components/helpers.js'
6
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
6
7
  import {
8
+ type ErrorMessageTemplateList,
7
9
  type FormPayload,
8
10
  type FormSubmissionError
9
11
  } from '~/src/server/plugins/engine/types.js'
@@ -21,13 +23,13 @@ export class TelephoneNumberField extends FormComponent {
21
23
  ) {
22
24
  super(def, props)
23
25
 
24
- const { options, title } = def
26
+ const { options } = def
25
27
 
26
28
  let formSchema = joi
27
29
  .string()
28
30
  .trim()
29
31
  .pattern(PATTERN)
30
- .label(title)
32
+ .label(this.label)
31
33
  .required()
32
34
 
33
35
  if (options.required === false) {
@@ -64,4 +66,17 @@ export class TelephoneNumberField extends FormComponent {
64
66
  type: 'tel'
65
67
  }
66
68
  }
69
+
70
+ /**
71
+ * For error preview page that shows all possible errors on a component
72
+ */
73
+ getAllPossibleErrors(): ErrorMessageTemplateList {
74
+ return {
75
+ baseErrors: [
76
+ { type: 'required', template: messageTemplate.required },
77
+ { type: 'format', template: messageTemplate.format }
78
+ ],
79
+ advancedSettingsErrors: []
80
+ }
81
+ }
67
82
  }
@@ -37,7 +37,7 @@ describe('TextField', () => {
37
37
  })
38
38
 
39
39
  describe('Schema', () => {
40
- it('uses component title as label', () => {
40
+ it('uses component title as label as default', () => {
41
41
  const { formSchema } = collection
42
42
  const { keys } = formSchema.describe()
43
43
 
@@ -196,10 +196,42 @@ describe('TextField', () => {
196
196
  )
197
197
  })
198
198
  })
199
+
200
+ describe('AllPossibleErrors', () => {
201
+ it('should return errors', () => {
202
+ const errors = field.getAllPossibleErrors()
203
+ expect(errors.baseErrors).not.toBeEmpty()
204
+ expect(errors.advancedSettingsErrors).not.toBeEmpty()
205
+ })
206
+ })
199
207
  })
200
208
 
201
209
  describe('Validation', () => {
202
210
  describe.each([
211
+ {
212
+ description: 'Use short description if it exists',
213
+ component: {
214
+ title: 'What is your example text?',
215
+ shortDescription: 'Your example text',
216
+ name: 'myComponent',
217
+ type: ComponentType.TextField,
218
+ options: {},
219
+ schema: {}
220
+ } satisfies TextFieldComponent,
221
+ assertions: [
222
+ {
223
+ input: getFormData(''),
224
+ output: {
225
+ value: getFormData(''),
226
+ errors: [
227
+ expect.objectContaining({
228
+ text: 'Enter your example text'
229
+ })
230
+ ]
231
+ }
232
+ }
233
+ ]
234
+ },
203
235
  {
204
236
  description: 'Trim empty spaces',
205
237
  component: {
@@ -8,7 +8,9 @@ import {
8
8
  FormComponent,
9
9
  isFormValue
10
10
  } from '~/src/server/plugins/engine/components/FormComponent.js'
11
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
11
12
  import {
13
+ type ErrorMessageTemplateList,
12
14
  type FormState,
13
15
  type FormStateValue,
14
16
  type FormSubmissionState
@@ -30,10 +32,10 @@ export class TextField extends FormComponent {
30
32
  ) {
31
33
  super(def, props)
32
34
 
33
- const { options, title } = def
35
+ const { options } = def
34
36
  const schema = 'schema' in def ? def.schema : {}
35
37
 
36
- let formSchema = joi.string().trim().label(title).required()
38
+ let formSchema = joi.string().trim().label(this.label).required()
37
39
 
38
40
  if (options.required === false) {
39
41
  formSchema = formSchema.allow('')
@@ -90,6 +92,19 @@ export class TextField extends FormComponent {
90
92
  return TextField.isText(value)
91
93
  }
92
94
 
95
+ /**
96
+ * For error preview page that shows all possible errors on a component
97
+ */
98
+ getAllPossibleErrors(): ErrorMessageTemplateList {
99
+ return {
100
+ baseErrors: [{ type: 'required', template: messageTemplate.required }],
101
+ advancedSettingsErrors: [
102
+ { type: 'min', template: messageTemplate.min },
103
+ { type: 'max', template: messageTemplate.max }
104
+ ]
105
+ }
106
+ }
107
+
93
108
  static isText(value?: FormStateValue | FormState): value is string {
94
109
  return isFormValue(value) && typeof value === 'string'
95
110
  }
@@ -465,6 +465,14 @@ describe('UkAddressField', () => {
465
465
  })
466
466
  })
467
467
  })
468
+
469
+ describe('AllPossibleErrors', () => {
470
+ it('should return errors', () => {
471
+ const errors = field.getAllPossibleErrors()
472
+ expect(errors.baseErrors).not.toBeEmpty()
473
+ expect(errors.advancedSettingsErrors).toBeEmpty()
474
+ })
475
+ })
468
476
  })
469
477
 
470
478
  describe('Validation', () => {
@@ -509,7 +517,8 @@ describe('UkAddressField', () => {
509
517
  postcode: ' WA4 1HT'
510
518
  }),
511
519
  output: {
512
- value: getFormData(address)
520
+ value: getFormData(address),
521
+ errors: undefined
513
522
  }
514
523
  },
515
524
  {
@@ -521,7 +530,8 @@ describe('UkAddressField', () => {
521
530
  postcode: 'WA4 1HT '
522
531
  }),
523
532
  output: {
524
- value: getFormData(address)
533
+ value: getFormData(address),
534
+ errors: undefined
525
535
  }
526
536
  },
527
537
  {
@@ -533,7 +543,8 @@ describe('UkAddressField', () => {
533
543
  postcode: ' WA4 1HT \n\n'
534
544
  }),
535
545
  output: {
536
- value: getFormData(address)
546
+ value: getFormData(address),
547
+ errors: undefined
537
548
  }
538
549
  }
539
550
  ]
@@ -661,6 +672,35 @@ describe('UkAddressField', () => {
661
672
  })
662
673
  ]
663
674
  }
675
+ },
676
+ {
677
+ input: getFormData({
678
+ addressLine1: '',
679
+ addressLine2: '',
680
+ town: '',
681
+ county: '',
682
+ postcode: postcodeInvalid
683
+ }),
684
+ output: {
685
+ value: getFormData({
686
+ addressLine1: '',
687
+ addressLine2: '',
688
+ town: '',
689
+ county: '',
690
+ postcode: postcodeInvalid
691
+ }),
692
+ errors: [
693
+ expect.objectContaining({
694
+ text: 'Enter address line 1'
695
+ }),
696
+ expect.objectContaining({
697
+ text: 'Enter town or city'
698
+ }),
699
+ expect.objectContaining({
700
+ text: 'Enter a valid postcode'
701
+ })
702
+ ]
703
+ }
664
704
  }
665
705
  ]
666
706
  }
@@ -676,6 +716,9 @@ describe('UkAddressField', () => {
676
716
  ({ input, output }) => {
677
717
  const result = collection.validate(input)
678
718
  expect(result).toEqual(output)
719
+
720
+ const errors = collection.getErrors(result.errors)
721
+ expect(errors).toEqual(output.errors)
679
722
  }
680
723
  )
681
724
  })
@@ -9,6 +9,7 @@ import {
9
9
  import { TextField } from '~/src/server/plugins/engine/components/TextField.js'
10
10
  import { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
11
11
  import {
12
+ type ErrorMessageTemplateList,
12
13
  type FormPayload,
13
14
  type FormState,
14
15
  type FormStateValue,
@@ -123,6 +124,18 @@ export class UkAddressField extends FormComponent {
123
124
  return Object.values(value).filter(Boolean)
124
125
  }
125
126
 
127
+ /**
128
+ * Returns one error per child field
129
+ */
130
+ getViewErrors(
131
+ errors?: FormSubmissionError[]
132
+ ): FormSubmissionError[] | undefined {
133
+ return this.getErrors(errors)?.filter(
134
+ (error, index, self) =>
135
+ index === self.findIndex((err) => err.name === error.name)
136
+ )
137
+ }
138
+
126
139
  getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
127
140
  const { collection, name, options } = this
128
141
 
@@ -163,6 +176,21 @@ export class UkAddressField extends FormComponent {
163
176
  return UkAddressField.isUkAddress(value)
164
177
  }
165
178
 
179
+ /**
180
+ * For error preview page that shows all possible errors on a component
181
+ */
182
+ getAllPossibleErrors(): ErrorMessageTemplateList {
183
+ return {
184
+ baseErrors: [
185
+ { type: 'required', template: 'Enter address line 1' },
186
+ { type: 'required', template: 'Enter town or city' },
187
+ { type: 'required', template: 'Enter postcode' },
188
+ { type: 'format', template: 'Enter valid postcode' }
189
+ ],
190
+ advancedSettingsErrors: []
191
+ }
192
+ }
193
+
166
194
  static isUkAddress(
167
195
  value?: FormStateValue | FormState
168
196
  ): value is UkAddressState {
@@ -121,7 +121,7 @@ describe('YesNoField', () => {
121
121
 
122
122
  expect(result.errors).toEqual([
123
123
  expect.objectContaining({
124
- text: 'Select example yes/no'
124
+ text: 'Example yes/no - select yes or no'
125
125
  })
126
126
  ])
127
127
  })
@@ -245,4 +245,12 @@ describe('YesNoField', () => {
245
245
  )
246
246
  })
247
247
  })
248
+
249
+ describe('AllPossibleErrors', () => {
250
+ it('should return errors', () => {
251
+ const errors = field.getAllPossibleErrors()
252
+ expect(errors.baseErrors).not.toBeEmpty()
253
+ expect(errors.advancedSettingsErrors).toBeEmpty()
254
+ })
255
+ })
248
256
  })
@@ -2,6 +2,9 @@ import { type YesNoFieldComponent } from '@defra/forms-model'
2
2
 
3
3
  import { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js'
4
4
  import { addClassOptionIfNone } from '~/src/server/plugins/engine/components/helpers.js'
5
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
6
+ import { type ErrorMessageTemplateList } from '~/src/server/plugins/engine/types.js'
7
+ import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js'
5
8
 
6
9
  /**
7
10
  * @description
@@ -25,7 +28,28 @@ export class YesNoField extends SelectionControlField {
25
28
  formSchema = formSchema.optional()
26
29
  }
27
30
 
31
+ formSchema = formSchema.messages(
32
+ convertToLanguageMessages({
33
+ 'any.required': messageTemplate.selectYesNoRequired
34
+ })
35
+ )
36
+
28
37
  this.formSchema = formSchema
29
38
  this.options = options
30
39
  }
40
+
41
+ /**
42
+ * For error preview page that shows all possible errors on a component
43
+ */
44
+ getAllPossibleErrors(): ErrorMessageTemplateList {
45
+ return {
46
+ baseErrors: [
47
+ {
48
+ type: 'selectYesNoRequired',
49
+ template: messageTemplate.selectYesNoRequired
50
+ }
51
+ ],
52
+ advancedSettingsErrors: []
53
+ }
54
+ }
31
55
  }
@@ -0,0 +1,24 @@
1
+ import { type ComponentDef } from '@defra/forms-model'
2
+
3
+ import { createComponent } from '~/src/server/plugins/engine/components/helpers.js'
4
+ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
5
+ import definition from '~/test/form/definitions/basic.js'
6
+
7
+ const formModel = new FormModel(definition, {
8
+ basePath: 'test'
9
+ })
10
+
11
+ describe('helpers tests', () => {
12
+ test('should throw if invalid type', () => {
13
+ expect(() =>
14
+ createComponent(
15
+ {
16
+ type: 'invalid-type'
17
+ } as unknown as ComponentDef,
18
+ {
19
+ model: formModel
20
+ }
21
+ )
22
+ ).toThrow('Component type invalid-type does not exist')
23
+ })
24
+ })
@@ -55,6 +55,8 @@ export type Component = InstanceType<
55
55
  // Field component instances only
56
56
  export type Field = InstanceType<
57
57
  | typeof Components.AutocompleteField
58
+ | typeof Components.RadiosField
59
+ | typeof Components.YesNoField
58
60
  | typeof Components.CheckboxesField
59
61
  | typeof Components.DatePartsField
60
62
  | typeof Components.EmailAddressField
@@ -72,10 +74,43 @@ export type Field = InstanceType<
72
74
  export type Guidance = InstanceType<
73
75
  | typeof Components.Details
74
76
  | typeof Components.Html
77
+ | typeof Components.Markdown
75
78
  | typeof Components.InsetText
76
79
  | typeof Components.List
77
80
  >
78
81
 
82
+ // List component instances only
83
+ export type ListField = InstanceType<
84
+ | typeof Components.AutocompleteField
85
+ | typeof Components.CheckboxesField
86
+ | typeof Components.RadiosField
87
+ | typeof Components.SelectField
88
+ | typeof Components.YesNoField
89
+ >
90
+
91
+ /**
92
+ * Filter known components with lists
93
+ */
94
+ export function hasListFormField(
95
+ field?: Partial<Component>
96
+ ): field is ListFormComponent {
97
+ return !!field && isListFieldType(field.type)
98
+ }
99
+
100
+ export function isListFieldType(
101
+ type?: ComponentType
102
+ ): type is ListField['type'] {
103
+ const allowedTypes = [
104
+ ComponentType.AutocompleteField,
105
+ ComponentType.CheckboxesField,
106
+ ComponentType.RadiosField,
107
+ ComponentType.SelectField,
108
+ ComponentType.YesNoField
109
+ ]
110
+
111
+ return !!type && allowedTypes.includes(type)
112
+ }
113
+
79
114
  /**
80
115
  * Create field instance for each {@link ComponentDef} type
81
116
  */
@@ -118,6 +153,10 @@ export function createComponent(
118
153
  component = new Components.List(def, options)
119
154
  break
120
155
 
156
+ case ComponentType.Markdown:
157
+ component = new Components.Markdown(def, options)
158
+ break
159
+
121
160
  case ComponentType.MultilineTextField:
122
161
  component = new Components.MultilineTextField(def, options)
123
162
  break
@@ -12,6 +12,7 @@ export { EmailAddressField } from '~/src/server/plugins/engine/components/EmailA
12
12
  export { Html } from '~/src/server/plugins/engine/components/Html.js'
13
13
  export { InsetText } from '~/src/server/plugins/engine/components/InsetText.js'
14
14
  export { List } from '~/src/server/plugins/engine/components/List.js'
15
+ export { Markdown } from '~/src/server/plugins/engine/components/Markdown.js'
15
16
  export { MultilineTextField } from '~/src/server/plugins/engine/components/MultilineTextField.js'
16
17
  export { NumberField } from '~/src/server/plugins/engine/components/NumberField.js'
17
18
  export { RadiosField } from '~/src/server/plugins/engine/components/RadiosField.js'
@@ -1,8 +1,8 @@
1
1
  import { join, parse } from 'node:path'
2
2
 
3
3
  import { type FormDefinition } from '@defra/forms-model'
4
- import { type ServerRegisterPluginObject } from '@hapi/hapi'
5
4
 
5
+ import { FORM_PREFIX } from '~/src/server/constants.js'
6
6
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
7
7
  import {
8
8
  plugin,
@@ -19,14 +19,24 @@ export const configureEnginePlugin = async ({
19
19
  formFilePath,
20
20
  services,
21
21
  controllers
22
- }: RouteConfig = {}): Promise<ServerRegisterPluginObject<PluginOptions>> => {
22
+ }: RouteConfig = {}): Promise<{
23
+ plugin: typeof plugin
24
+ options: PluginOptions
25
+ }> => {
23
26
  let model: FormModel | undefined
24
27
 
25
28
  if (formFileName && formFilePath) {
26
29
  const definition = await getForm(join(formFilePath, formFileName))
27
30
  const { name } = parse(formFileName)
28
31
 
29
- model = new FormModel(definition, { basePath: name }, services, controllers)
32
+ const initialBasePath = `${FORM_PREFIX}${name}`
33
+
34
+ model = new FormModel(
35
+ definition,
36
+ { basePath: initialBasePath },
37
+ services,
38
+ controllers
39
+ )
30
40
  }
31
41
 
32
42
  return {