@defra/forms-engine-plugin 0.1.11 → 0.1.12

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 +6 -5
  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 +4 -4
  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
@@ -2,6 +2,7 @@ import {
2
2
  ComponentType,
3
3
  type AutocompleteFieldComponent
4
4
  } from '@defra/forms-model'
5
+ import lowerFirst from 'lodash/lowerFirst.js'
5
6
 
6
7
  import { AutocompleteField } from '~/src/server/plugins/engine/components/AutocompleteField.js'
7
8
  import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
@@ -32,7 +33,8 @@ describe.each([
32
33
  options: {
33
34
  list: listString,
34
35
  examples: listStringExamples,
35
- allow: ['1', '2', '3', '4']
36
+ allow: ['1', '2', '3', '4'],
37
+ shortDescription: 'My string list'
36
38
  }
37
39
  },
38
40
  {
@@ -47,7 +49,8 @@ describe.each([
47
49
  options: {
48
50
  list: listNumber,
49
51
  examples: listNumberExamples,
50
- allow: [1, 2, 3, 4]
52
+ allow: [1, 2, 3, 4],
53
+ shortDescription: 'My number list'
51
54
  }
52
55
  }
53
56
  ])('AutocompleteField: $component.title', ({ component: def, options }) => {
@@ -153,7 +156,35 @@ describe.each([
153
156
 
154
157
  expect(result.errors).toEqual([
155
158
  expect.objectContaining({
156
- text: `Enter ${def.title.toLowerCase()}`
159
+ text: `Enter ${lowerFirst(def.title)}`
160
+ })
161
+ ])
162
+ })
163
+
164
+ it('adds errors for empty value if shortDescription exists', () => {
165
+ collection = new ComponentCollection(
166
+ [{ ...def, shortDescription: options.shortDescription }],
167
+ { model }
168
+ )
169
+ const result = collection.validate(getFormData(''))
170
+
171
+ expect(result.errors).toEqual([
172
+ expect.objectContaining({
173
+ text: `Enter ${lowerFirst(options.shortDescription)}`
174
+ })
175
+ ])
176
+ })
177
+
178
+ it('adds errors for empty value if shortDescription exists but is empty', () => {
179
+ collection = new ComponentCollection(
180
+ [{ ...def, shortDescription: '' }],
181
+ { model }
182
+ )
183
+ const result = collection.validate(getFormData(''))
184
+
185
+ expect(result.errors).toEqual([
186
+ expect.objectContaining({
187
+ text: `Enter ${lowerFirst(def.title)}`
157
188
  })
158
189
  ])
159
190
  })
@@ -290,5 +321,42 @@ describe.each([
290
321
  expect(items).toEqual([])
291
322
  })
292
323
  })
324
+
325
+ describe('AllPossibleErrors', () => {
326
+ it('should return errors', () => {
327
+ const errors = field.getAllPossibleErrors()
328
+ expect(errors.baseErrors).not.toBeEmpty()
329
+ expect(errors.advancedSettingsErrors).toBeEmpty()
330
+ })
331
+ })
332
+
333
+ describe('Validation', () => {
334
+ describe.each([
335
+ {
336
+ description: 'Use short description if it exists',
337
+ component: {
338
+ title: 'What is your example text?',
339
+ shortDescription: 'Your example text',
340
+ name: 'myComponent',
341
+ type: ComponentType.AutocompleteField,
342
+ list: 'ABCE',
343
+ options: {}
344
+ } satisfies AutocompleteFieldComponent,
345
+ assertions: [
346
+ {
347
+ input: getFormData(''),
348
+ output: {
349
+ value: getFormData(''),
350
+ errors: [
351
+ expect.objectContaining({
352
+ text: 'Enter your example text'
353
+ })
354
+ ]
355
+ }
356
+ }
357
+ ]
358
+ }
359
+ ])
360
+ })
293
361
  })
294
362
  })
@@ -23,8 +23,12 @@ export class AutocompleteField extends SelectField {
23
23
  const messages = options.customValidationMessages
24
24
 
25
25
  formSchema = formSchema.messages({
26
- 'any.only': messages?.['any.only'] ?? messageTemplate.required,
27
- 'any.required': messages?.['any.required'] ?? messageTemplate.required
26
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
27
+ 'any.only':
28
+ messages?.['any.only'] ?? (messageTemplate.required as string),
29
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
30
+ 'any.required':
31
+ messages?.['any.required'] ?? (messageTemplate.required as string)
28
32
  })
29
33
  }
30
34
 
@@ -2,6 +2,8 @@ import {
2
2
  ComponentType,
3
3
  type CheckboxesFieldComponent
4
4
  } from '@defra/forms-model'
5
+ import { toLower } from 'lodash'
6
+ import lowerFirst from 'lodash/lowerFirst.js'
5
7
  import { outdent } from 'outdent'
6
8
 
7
9
  import { CheckboxesField } from '~/src/server/plugins/engine/components/CheckboxesField.js'
@@ -23,7 +25,8 @@ import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
23
25
  describe.each([
24
26
  {
25
27
  component: {
26
- title: 'String list',
28
+ title: 'String list title',
29
+ shortDescription: 'String list',
27
30
  name: 'myComponent',
28
31
  type: ComponentType.CheckboxesField,
29
32
  list: 'listString',
@@ -31,6 +34,7 @@ describe.each([
31
34
  } satisfies CheckboxesFieldComponent,
32
35
 
33
36
  options: {
37
+ label: 'string list',
34
38
  list: listString,
35
39
  examples: listStringExamples,
36
40
  allow: ['1', '2', '3', '4'],
@@ -39,14 +43,34 @@ describe.each([
39
43
  },
40
44
  {
41
45
  component: {
42
- title: 'Number list',
46
+ title: 'String list title',
47
+ shortDescription: 'String list',
43
48
  name: 'myComponent',
44
49
  type: ComponentType.CheckboxesField,
50
+ list: 'listString',
51
+ options: {}
52
+ } satisfies CheckboxesFieldComponent,
53
+
54
+ options: {
55
+ label: 'string list',
56
+ list: listString,
57
+ examples: listStringExamples,
58
+ allow: ['1', '2', '3', '4'],
59
+ deny: ['5', '6', '7', '8']
60
+ }
61
+ },
62
+ {
63
+ component: {
64
+ title: 'Number list title',
65
+ name: 'myComponent',
66
+ shortDescription: 'Number list',
67
+ type: ComponentType.CheckboxesField,
45
68
  list: 'listNumber',
46
69
  options: {}
47
70
  } satisfies CheckboxesFieldComponent,
48
71
 
49
72
  options: {
73
+ label: 'number list',
50
74
  list: listNumber,
51
75
  examples: listNumberExamples,
52
76
  allow: [1, 2, 3, 4],
@@ -72,14 +96,14 @@ describe.each([
72
96
 
73
97
  describe('Defaults', () => {
74
98
  describe('Schema', () => {
75
- it('uses component title as label', () => {
99
+ it('uses component short description as label', () => {
76
100
  const { formSchema } = collection
77
101
  const { keys } = formSchema.describe()
78
102
 
79
103
  expect(keys).toHaveProperty(
80
104
  'myComponent',
81
105
  expect.objectContaining({
82
- flags: expect.objectContaining({ label: def.title })
106
+ flags: expect.objectContaining({ label: def.shortDescription })
83
107
  })
84
108
  )
85
109
  })
@@ -157,7 +181,7 @@ describe.each([
157
181
  {
158
182
  allow: options.allow,
159
183
  flags: {
160
- label: def.title,
184
+ label: def.shortDescription,
161
185
  only: true
162
186
  },
163
187
  type: options.list.type
@@ -172,7 +196,7 @@ describe.each([
172
196
 
173
197
  expect(result.errors).toEqual([
174
198
  expect.objectContaining({
175
- text: `Select ${def.title.toLowerCase()}`
199
+ text: `Select ${lowerFirst(options.label)}`
176
200
  })
177
201
  ])
178
202
  })
@@ -200,7 +224,7 @@ describe.each([
200
224
 
201
225
  expect(result.errors).toEqual([
202
226
  expect.objectContaining({
203
- text: `Select ${def.title.toLowerCase()}`
227
+ text: `Select ${toLower(def.shortDescription)}`
204
228
  })
205
229
  ])
206
230
  }
@@ -213,7 +237,7 @@ describe.each([
213
237
 
214
238
  expect(result.errors).toEqual([
215
239
  expect.objectContaining({
216
- text: `Select ${def.title.toLowerCase()}`
240
+ text: `Select ${lowerFirst(options.label)}`
217
241
  })
218
242
  ])
219
243
  }
@@ -375,5 +399,13 @@ describe.each([
375
399
  expect(items).toEqual([])
376
400
  })
377
401
  })
402
+
403
+ describe('AllPossibleErrors', () => {
404
+ it('should return errors', () => {
405
+ const errors = field.getAllPossibleErrors()
406
+ expect(errors.baseErrors).not.toBeEmpty()
407
+ expect(errors.advancedSettingsErrors).toBeEmpty()
408
+ })
409
+ })
378
410
  })
379
411
  })
@@ -23,16 +23,20 @@ export class CheckboxesField extends SelectionControlField {
23
23
  super(def, props)
24
24
 
25
25
  const { listType: type } = this
26
- const { options, title } = def
26
+ const { options } = def
27
27
 
28
28
  let formSchema =
29
29
  type === 'string' ? joi.array<string>() : joi.array<number>()
30
30
 
31
31
  const itemsSchema = joi[type]()
32
32
  .valid(...this.values)
33
- .label(title)
33
+ .label(this.label)
34
34
 
35
- formSchema = formSchema.items(itemsSchema).single().label(title).required()
35
+ formSchema = formSchema
36
+ .items(itemsSchema)
37
+ .single()
38
+ .label(this.label)
39
+ .required()
36
40
 
37
41
  if (options.required === false) {
38
42
  formSchema = formSchema.optional()
@@ -124,7 +124,7 @@ export class ComponentCollection {
124
124
  }
125
125
 
126
126
  // Update error with parent title
127
- error.local.title ??= parent?.title
127
+ error.local.title ??= parent?.label
128
128
 
129
129
  return error
130
130
  })
@@ -212,25 +212,22 @@ export class ComponentCollection {
212
212
  return context
213
213
  }
214
214
 
215
+ /**
216
+ * Get all errors for all fields in this collection
217
+ */
215
218
  getErrors(errors?: FormSubmissionError[]): FormSubmissionError[] | undefined {
216
- const { fields } = this
217
-
218
- const list: FormSubmissionError[] = []
219
-
220
- // Add only one error per field
221
- for (const field of fields) {
222
- const error = field.getError(errors)
223
-
224
- if (error) {
225
- list.push(error)
226
- }
227
- }
228
-
229
- if (!list.length) {
230
- return
231
- }
219
+ return this.getFieldErrors((field) => field.getErrors(errors), errors)
220
+ }
232
221
 
233
- return list
222
+ /**
223
+ * Get view errors for all fields in this collection.
224
+ * For most fields this means filtering to the first error in the list.
225
+ * Composite fields like UKAddress can choose to return more than one error.
226
+ */
227
+ getViewErrors(
228
+ errors?: FormSubmissionError[]
229
+ ): FormSubmissionError[] | undefined {
230
+ return this.getFieldErrors((field) => field.getViewErrors(errors), errors)
234
231
  }
235
232
 
236
233
  getViewModel(
@@ -266,6 +263,36 @@ export class ComponentCollection {
266
263
  errors: this.page?.getErrors(details) ?? getErrors(details)
267
264
  }
268
265
  }
266
+
267
+ /**
268
+ * Helper to get errors from all fields
269
+ */
270
+ private getFieldErrors(
271
+ callback: (field: Field) => FormSubmissionError[] | undefined,
272
+ errors?: FormSubmissionError[]
273
+ ): FormSubmissionError[] | undefined {
274
+ const { fields } = this
275
+
276
+ if (!errors?.length) {
277
+ return
278
+ }
279
+
280
+ const list: FormSubmissionError[] = []
281
+
282
+ for (const field of fields) {
283
+ const fieldErrors = callback(field)
284
+
285
+ if (fieldErrors?.length) {
286
+ list.push(...fieldErrors)
287
+ }
288
+ }
289
+
290
+ if (!list.length) {
291
+ return
292
+ }
293
+
294
+ return list
295
+ }
269
296
  }
270
297
 
271
298
  /**
@@ -31,6 +31,7 @@ describe('DatePartsField', () => {
31
31
  beforeEach(() => {
32
32
  def = {
33
33
  title: 'Example date parts field',
34
+ shortDescription: 'Example date parts',
34
35
  name: 'myComponent',
35
36
  type: ComponentType.DatePartsField,
36
37
  options: {}
@@ -199,7 +200,7 @@ describe('DatePartsField', () => {
199
200
  expect(result3.errors).toBeUndefined()
200
201
  })
201
202
 
202
- it('adds errors for empty value', () => {
203
+ it('adds errors for empty value when short description exists', () => {
203
204
  const result = collection.validate(
204
205
  getFormData({
205
206
  day: '',
@@ -210,13 +211,13 @@ describe('DatePartsField', () => {
210
211
 
211
212
  expect(result.errors).toEqual([
212
213
  expect.objectContaining({
213
- text: 'Example date parts field must include a day'
214
+ text: 'Example date parts must include a day'
214
215
  }),
215
216
  expect.objectContaining({
216
- text: 'Example date parts field must include a month'
217
+ text: 'Example date parts must include a month'
217
218
  }),
218
219
  expect.objectContaining({
219
- text: 'Example date parts field must include a year'
220
+ text: 'Example date parts must include a year'
220
221
  })
221
222
  ])
222
223
  })
@@ -409,6 +410,14 @@ describe('DatePartsField', () => {
409
410
  })
410
411
  })
411
412
  })
413
+
414
+ describe('AllPossibleErrors', () => {
415
+ it('should return errors', () => {
416
+ const errors = field.getAllPossibleErrors()
417
+ expect(errors.baseErrors).not.toBeEmpty()
418
+ expect(errors.advancedSettingsErrors).not.toBeEmpty()
419
+ })
420
+ })
412
421
  })
413
422
 
414
423
  describe('Validation', () => {
@@ -1,11 +1,6 @@
1
1
  import { ComponentType, type DatePartsFieldComponent } from '@defra/forms-model'
2
2
  import { add, format, isValid, parse, startOfToday, sub } from 'date-fns'
3
- import {
4
- type Context,
5
- type CustomValidator,
6
- type LanguageMessages,
7
- type ObjectSchema
8
- } from 'joi'
3
+ import { type Context, type CustomValidator, type ObjectSchema } from 'joi'
9
4
 
10
5
  import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
11
6
  import {
@@ -17,12 +12,14 @@ import { NumberField } from '~/src/server/plugins/engine/components/NumberField.
17
12
  import { type DateInputItem } from '~/src/server/plugins/engine/components/types.js'
18
13
  import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
19
14
  import {
15
+ type ErrorMessageTemplateList,
20
16
  type FormPayload,
21
17
  type FormState,
22
18
  type FormStateValue,
23
19
  type FormSubmissionError,
24
20
  type FormSubmissionState
25
21
  } from '~/src/server/plugins/engine/types.js'
22
+ import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js'
26
23
 
27
24
  export class DatePartsField extends FormComponent {
28
25
  declare options: DatePartsFieldComponent['options']
@@ -40,15 +37,17 @@ export class DatePartsField extends FormComponent {
40
37
 
41
38
  const isRequired = options.required !== false
42
39
 
43
- const customValidationMessages: LanguageMessages = {
40
+ const customValidationMessages = convertToLanguageMessages({
41
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
44
42
  'any.required': messageTemplate.objectMissing,
43
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
45
44
  'number.base': messageTemplate.objectMissing,
46
45
  'number.precision': messageTemplate.dateFormat,
47
46
  'number.integer': messageTemplate.dateFormat,
48
47
  'number.unsafe': messageTemplate.dateFormat,
49
48
  'number.min': messageTemplate.dateFormat,
50
49
  'number.max': messageTemplate.dateFormat
51
- }
50
+ })
52
51
 
53
52
  this.collection = new ComponentCollection(
54
53
  [
@@ -192,6 +191,28 @@ export class DatePartsField extends FormComponent {
192
191
  return DatePartsField.isDateParts(value)
193
192
  }
194
193
 
194
+ /**
195
+ * For error preview page that shows all possible errors on a component
196
+ */
197
+ getAllPossibleErrors(): ErrorMessageTemplateList {
198
+ return {
199
+ baseErrors: [
200
+ { type: 'required', template: messageTemplate.required },
201
+ { type: 'dateFormat', template: messageTemplate.dateFormat },
202
+ { type: 'dateFormatDay', template: '{{#label}} must include a day' },
203
+ {
204
+ type: 'dateFormatMonth',
205
+ template: '{{#label}} must include a month'
206
+ },
207
+ { type: 'dateFormatYear', template: '{{#label}} must include a year' }
208
+ ],
209
+ advancedSettingsErrors: [
210
+ { type: 'dateMin', template: messageTemplate.dateMin },
211
+ { type: 'dateMax', template: messageTemplate.dateMax }
212
+ ]
213
+ }
214
+ }
215
+
195
216
  static isDateParts(
196
217
  value?: FormStateValue | FormState
197
218
  ): value is DatePartsState {
@@ -29,6 +29,7 @@ describe('EmailAddressField', () => {
29
29
  beforeEach(() => {
30
30
  def = {
31
31
  title: 'Example email address field',
32
+ shortDescription: 'Example email address',
32
33
  name: 'myComponent',
33
34
  type: ComponentType.EmailAddressField,
34
35
  options: {}
@@ -47,7 +48,7 @@ describe('EmailAddressField', () => {
47
48
  'myComponent',
48
49
  expect.objectContaining({
49
50
  flags: expect.objectContaining({
50
- label: 'Example email address field'
51
+ label: 'Example email address'
51
52
  })
52
53
  })
53
54
  )
@@ -111,6 +112,24 @@ describe('EmailAddressField', () => {
111
112
  it('adds errors for empty value', () => {
112
113
  const result = collection.validate(getFormData(''))
113
114
 
115
+ expect(result.errors).toEqual([
116
+ expect.objectContaining({
117
+ text: 'Enter example email address'
118
+ })
119
+ ])
120
+ })
121
+
122
+ it('adds errors for empty value given no shortDescription', () => {
123
+ def = {
124
+ title: 'Example email address field',
125
+ name: 'myComponent',
126
+ type: ComponentType.EmailAddressField,
127
+ options: {}
128
+ } satisfies EmailAddressFieldComponent
129
+
130
+ collection = new ComponentCollection([def], { model })
131
+ const result = collection.validate(getFormData(''))
132
+
114
133
  expect(result.errors).toEqual([
115
134
  expect.objectContaining({
116
135
  text: 'Enter example email address field'
@@ -205,6 +224,14 @@ describe('EmailAddressField', () => {
205
224
  )
206
225
  })
207
226
  })
227
+
228
+ describe('AllPossibleErrors', () => {
229
+ it('should return errors', () => {
230
+ const errors = field.getAllPossibleErrors()
231
+ expect(errors.baseErrors).not.toBeEmpty()
232
+ expect(errors.advancedSettingsErrors).toBeEmpty()
233
+ })
234
+ })
208
235
  })
209
236
 
210
237
  describe('Validation', () => {
@@ -269,6 +296,29 @@ describe('EmailAddressField', () => {
269
296
  }
270
297
  ]
271
298
  },
299
+ {
300
+ description: 'Email address validation',
301
+ component: {
302
+ title: 'Example email address field',
303
+ shortDescription: 'Example email address',
304
+ name: 'myComponent',
305
+ type: ComponentType.EmailAddressField,
306
+ options: {}
307
+ } satisfies EmailAddressFieldComponent,
308
+ assertions: [
309
+ {
310
+ input: getFormData('defra.helpline'),
311
+ output: {
312
+ value: getFormData('defra.helpline'),
313
+ errors: [
314
+ expect.objectContaining({
315
+ text: 'Enter example email address in the correct format'
316
+ })
317
+ ]
318
+ }
319
+ }
320
+ ]
321
+ },
272
322
  {
273
323
  description: 'Custom validation message',
274
324
  component: {
@@ -2,7 +2,9 @@ import { type EmailAddressFieldComponent } from '@defra/forms-model'
2
2
  import joi from 'joi'
3
3
 
4
4
  import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
5
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
5
6
  import {
7
+ type ErrorMessageTemplateList,
6
8
  type FormPayload,
7
9
  type FormSubmissionError
8
10
  } from '~/src/server/plugins/engine/types.js'
@@ -16,9 +18,9 @@ export class EmailAddressField extends FormComponent {
16
18
  ) {
17
19
  super(def, props)
18
20
 
19
- const { options, title } = def
21
+ const { options } = def
20
22
 
21
- let formSchema = joi.string().email().trim().label(title).required()
23
+ let formSchema = joi.string().email().trim().label(this.label).required()
22
24
 
23
25
  if (options.required === false) {
24
26
  formSchema = formSchema.allow('')
@@ -52,4 +54,17 @@ export class EmailAddressField extends FormComponent {
52
54
  type: 'email'
53
55
  }
54
56
  }
57
+
58
+ /**
59
+ * For error preview page that shows all possible errors on a component
60
+ */
61
+ getAllPossibleErrors(): ErrorMessageTemplateList {
62
+ return {
63
+ baseErrors: [
64
+ { type: 'required', template: messageTemplate.required },
65
+ { type: 'format', template: messageTemplate.format }
66
+ ],
67
+ advancedSettingsErrors: []
68
+ }
69
+ }
55
70
  }
@@ -157,6 +157,7 @@ describe('FileUploadField', () => {
157
157
  beforeEach(() => {
158
158
  def = {
159
159
  title: 'Example file upload field',
160
+ shortDescription: 'Example file upload',
160
161
  name: 'myComponent',
161
162
  type: ComponentType.FileUploadField,
162
163
  options: {},
@@ -169,7 +170,32 @@ describe('FileUploadField', () => {
169
170
  })
170
171
 
171
172
  describe('Schema', () => {
173
+ it('uses component short description as label', () => {
174
+ const { formSchema } = collection
175
+ const { keys } = formSchema.describe()
176
+
177
+ expect(keys).toHaveProperty(
178
+ 'myComponent',
179
+ expect.objectContaining({
180
+ flags: expect.objectContaining({
181
+ label: 'Example file upload'
182
+ })
183
+ })
184
+ )
185
+ })
186
+
172
187
  it('uses component title as label', () => {
188
+ def = {
189
+ title: 'Example file upload field',
190
+ name: 'myComponent',
191
+ type: ComponentType.FileUploadField,
192
+ options: {},
193
+ schema: {}
194
+ } satisfies FileUploadFieldComponent
195
+
196
+ page = createPage(model, definition.pages[0])
197
+ collection = new ComponentCollection([def], { page, model })
198
+
173
199
  const { formSchema } = collection
174
200
  const { keys } = formSchema.describe()
175
201
 
@@ -246,6 +272,25 @@ describe('FileUploadField', () => {
246
272
  it('adds errors for empty value', () => {
247
273
  const result = collection.validate(getFormData())
248
274
 
275
+ expect(result.errors).toEqual([
276
+ expect.objectContaining({
277
+ text: 'Select example file upload'
278
+ })
279
+ ])
280
+ })
281
+
282
+ it('adds errors for empty value with no shortDescription', () => {
283
+ def = {
284
+ title: 'Example file upload field',
285
+ name: 'myComponent',
286
+ type: ComponentType.FileUploadField,
287
+ options: {},
288
+ schema: {}
289
+ } satisfies FileUploadFieldComponent
290
+
291
+ collection = new ComponentCollection([def], { model })
292
+ const result = collection.validate(getFormData())
293
+
249
294
  expect(result.errors).toEqual([
250
295
  expect.objectContaining({
251
296
  text: 'Select example file upload field'
@@ -545,6 +590,14 @@ describe('FileUploadField', () => {
545
590
  )
546
591
  })
547
592
  })
593
+
594
+ describe('AllPossibleErrors', () => {
595
+ it('should return errors', () => {
596
+ const errors = field.getAllPossibleErrors()
597
+ expect(errors.baseErrors).not.toBeEmpty()
598
+ expect(errors.advancedSettingsErrors).not.toBeEmpty()
599
+ })
600
+ })
548
601
  })
549
602
 
550
603
  describe('Validation', () => {