@defra/forms-engine-plugin 4.0.5 → 4.0.7

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 (83) hide show
  1. package/.public/stylesheets/application.min.css +2 -2
  2. package/.public/stylesheets/application.min.css.map +1 -1
  3. package/.server/client/stylesheets/_location-input.scss +60 -0
  4. package/.server/client/stylesheets/application.scss +1 -6
  5. package/.server/client/stylesheets/shared.scss +8 -0
  6. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +28 -0
  7. package/.server/server/plugins/engine/components/ComponentBase.d.ts +1 -1
  8. package/.server/server/plugins/engine/components/ComponentBase.js.map +1 -1
  9. package/.server/server/plugins/engine/components/EastingNorthingField.d.ts +121 -0
  10. package/.server/server/plugins/engine/components/EastingNorthingField.js +166 -0
  11. package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -0
  12. package/.server/server/plugins/engine/components/LatLongField.d.ts +121 -0
  13. package/.server/server/plugins/engine/components/LatLongField.js +164 -0
  14. package/.server/server/plugins/engine/components/LatLongField.js.map +1 -0
  15. package/.server/server/plugins/engine/components/LocationFieldBase.d.ts +134 -0
  16. package/.server/server/plugins/engine/components/LocationFieldBase.js +85 -0
  17. package/.server/server/plugins/engine/components/LocationFieldBase.js.map +1 -0
  18. package/.server/server/plugins/engine/components/LocationFieldHelpers.d.ts +108 -0
  19. package/.server/server/plugins/engine/components/LocationFieldHelpers.js +96 -0
  20. package/.server/server/plugins/engine/components/LocationFieldHelpers.js.map +1 -0
  21. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.d.ts +19 -0
  22. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js +40 -0
  23. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js.map +1 -0
  24. package/.server/server/plugins/engine/components/OsGridRefField.d.ts +19 -0
  25. package/.server/server/plugins/engine/components/OsGridRefField.js +56 -0
  26. package/.server/server/plugins/engine/components/OsGridRefField.js.map +1 -0
  27. package/.server/server/plugins/engine/components/helpers/components.d.ts +3 -4
  28. package/.server/server/plugins/engine/components/helpers/components.js +21 -29
  29. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  30. package/.server/server/plugins/engine/components/index.d.ts +4 -0
  31. package/.server/server/plugins/engine/components/index.js +4 -0
  32. package/.server/server/plugins/engine/components/index.js.map +1 -1
  33. package/.server/server/plugins/engine/components/markdownParser.d.ts +2 -0
  34. package/.server/server/plugins/engine/components/markdownParser.js +28 -0
  35. package/.server/server/plugins/engine/components/markdownParser.js.map +1 -0
  36. package/.server/server/plugins/engine/components/types.d.ts +10 -0
  37. package/.server/server/plugins/engine/components/types.js.map +1 -1
  38. package/.server/server/plugins/engine/pageControllers/helpers/pages.js +7 -0
  39. package/.server/server/plugins/engine/pageControllers/helpers/pages.js.map +1 -1
  40. package/.server/server/plugins/engine/types/index.d.ts +1 -1
  41. package/.server/server/plugins/engine/types/index.js.map +1 -1
  42. package/.server/server/plugins/engine/types.d.ts +2 -2
  43. package/.server/server/plugins/engine/types.js.map +1 -1
  44. package/.server/server/plugins/engine/views/components/_location-field-base.html +53 -0
  45. package/.server/server/plugins/engine/views/components/eastingnorthingfield.html +5 -0
  46. package/.server/server/plugins/engine/views/components/latlongfield.html +5 -0
  47. package/.server/server/plugins/engine/views/components/nationalgridfieldnumberfield.html +13 -0
  48. package/.server/server/plugins/engine/views/components/osgridreffield.html +13 -0
  49. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  50. package/package.json +3 -3
  51. package/src/client/stylesheets/_location-input.scss +60 -0
  52. package/src/client/stylesheets/application.scss +1 -6
  53. package/src/client/stylesheets/shared.scss +8 -0
  54. package/src/server/forms/register-as-a-unicorn-breeder.yaml +28 -0
  55. package/src/server/plugins/engine/components/ComponentBase.ts +1 -1
  56. package/src/server/plugins/engine/components/EastingNorthingField.test.ts +665 -0
  57. package/src/server/plugins/engine/components/EastingNorthingField.ts +224 -0
  58. package/src/server/plugins/engine/components/LatLongField.test.ts +700 -0
  59. package/src/server/plugins/engine/components/LatLongField.ts +213 -0
  60. package/src/server/plugins/engine/components/LocationFieldBase.test.ts +253 -0
  61. package/src/server/plugins/engine/components/LocationFieldBase.ts +152 -0
  62. package/src/server/plugins/engine/components/LocationFieldHelpers.test.ts +338 -0
  63. package/src/server/plugins/engine/components/LocationFieldHelpers.ts +123 -0
  64. package/src/server/plugins/engine/components/NationalGridFieldNumberField.test.ts +438 -0
  65. package/src/server/plugins/engine/components/NationalGridFieldNumberField.ts +52 -0
  66. package/src/server/plugins/engine/components/OsGridRefField.test.ts +469 -0
  67. package/src/server/plugins/engine/components/OsGridRefField.ts +71 -0
  68. package/src/server/plugins/engine/components/helpers/components.test.ts +270 -0
  69. package/src/server/plugins/engine/components/helpers/components.ts +39 -47
  70. package/src/server/plugins/engine/components/helpers/helpers.test.ts +71 -1
  71. package/src/server/plugins/engine/components/index.ts +4 -0
  72. package/src/server/plugins/engine/components/markdownParser.ts +40 -0
  73. package/src/server/plugins/engine/components/types.ts +14 -0
  74. package/src/server/plugins/engine/outputFormatters/adapter/v1.location.test.ts +356 -0
  75. package/src/server/plugins/engine/pageControllers/helpers/helpers.test.ts +4 -0
  76. package/src/server/plugins/engine/pageControllers/helpers/pages.ts +8 -0
  77. package/src/server/plugins/engine/types/index.ts +2 -0
  78. package/src/server/plugins/engine/types.ts +4 -0
  79. package/src/server/plugins/engine/views/components/_location-field-base.html +53 -0
  80. package/src/server/plugins/engine/views/components/eastingnorthingfield.html +5 -0
  81. package/src/server/plugins/engine/views/components/latlongfield.html +5 -0
  82. package/src/server/plugins/engine/views/components/nationalgridfieldnumberfield.html +13 -0
  83. package/src/server/plugins/engine/views/components/osgridreffield.html +13 -0
@@ -0,0 +1,438 @@
1
+ import {
2
+ ComponentType,
3
+ type NationalGridFieldNumberFieldComponent
4
+ } from '@defra/forms-model'
5
+
6
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
7
+ import { NationalGridFieldNumberField } from '~/src/server/plugins/engine/components/NationalGridFieldNumberField.js'
8
+ import {
9
+ getAnswer,
10
+ type Field
11
+ } from '~/src/server/plugins/engine/components/helpers/components.js'
12
+ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
13
+ import definition from '~/test/form/definitions/blank.js'
14
+ import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
15
+
16
+ describe('NationalGridFieldNumberField', () => {
17
+ let model: FormModel
18
+
19
+ beforeEach(() => {
20
+ model = new FormModel(definition, {
21
+ basePath: 'test'
22
+ })
23
+ })
24
+
25
+ describe('Defaults', () => {
26
+ let def: NationalGridFieldNumberFieldComponent
27
+ let collection: ComponentCollection
28
+ let field: Field
29
+
30
+ beforeEach(() => {
31
+ def = {
32
+ title: 'Example National Grid field number',
33
+ name: 'myComponent',
34
+ type: ComponentType.NationalGridFieldNumberField,
35
+ options: {}
36
+ }
37
+
38
+ collection = new ComponentCollection([def], { model })
39
+ field = collection.fields[0]
40
+ })
41
+
42
+ describe('Schema', () => {
43
+ it('uses component title as label as default', () => {
44
+ const { formSchema } = collection
45
+ const { keys } = formSchema.describe()
46
+
47
+ expect(keys).toHaveProperty(
48
+ 'myComponent',
49
+ expect.objectContaining({
50
+ flags: expect.objectContaining({
51
+ label: 'Example National Grid field number'
52
+ })
53
+ })
54
+ )
55
+ })
56
+
57
+ it('uses component name as keys', () => {
58
+ const { formSchema } = collection
59
+ const { keys } = formSchema.describe()
60
+
61
+ expect(field.keys).toEqual(['myComponent'])
62
+ expect(field.collection).toBeUndefined()
63
+
64
+ for (const key of field.keys) {
65
+ expect(keys).toHaveProperty(key)
66
+ }
67
+ })
68
+
69
+ it('is required by default', () => {
70
+ const { formSchema } = collection
71
+ const { keys } = formSchema.describe()
72
+
73
+ expect(keys).toHaveProperty(
74
+ 'myComponent',
75
+ expect.objectContaining({
76
+ flags: expect.objectContaining({
77
+ presence: 'required'
78
+ })
79
+ })
80
+ )
81
+ })
82
+
83
+ it('is optional when configured', () => {
84
+ const collectionOptional = new ComponentCollection(
85
+ [{ ...def, options: { required: false } }],
86
+ { model }
87
+ )
88
+
89
+ const { formSchema } = collectionOptional
90
+ const { keys } = formSchema.describe()
91
+
92
+ expect(keys).toHaveProperty(
93
+ 'myComponent',
94
+ expect.objectContaining({ allow: [''] })
95
+ )
96
+
97
+ const result = collectionOptional.validate(getFormData(''))
98
+ expect(result.errors).toBeUndefined()
99
+ })
100
+
101
+ it('accepts valid values', () => {
102
+ const result1 = collection.validate(getFormData('NG12345678'))
103
+ const result2 = collection.validate(getFormData('ng12345678'))
104
+ const result3 = collection.validate(getFormData('AB98765432'))
105
+
106
+ expect(result1.errors).toBeUndefined()
107
+ expect(result2.errors).toBeUndefined()
108
+ expect(result3.errors).toBeUndefined()
109
+ })
110
+
111
+ it('formats values with spaces per GDS guidance', () => {
112
+ const result1 = collection.validate(getFormData('NG 1234 5678'))
113
+ const result2 = collection.validate(getFormData('NG12345678'))
114
+ const result3 = collection.validate(getFormData('NG12345,678'))
115
+
116
+ expect(result1.value.myComponent).toBe('NG 1234 5678')
117
+ expect(result2.value.myComponent).toBe('NG 1234 5678')
118
+ expect(result3.value.myComponent).toBe('NG 1234 5678')
119
+ })
120
+
121
+ it('adds errors for empty value', () => {
122
+ const result = collection.validate(getFormData(''))
123
+
124
+ expect(result.errors).toEqual([
125
+ expect.objectContaining({
126
+ text: 'Enter example National Grid field number'
127
+ })
128
+ ])
129
+ })
130
+
131
+ it('adds errors for invalid values', () => {
132
+ const result1 = collection.validate(getFormData('NG1234567'))
133
+ const result2 = collection.validate(getFormData('N123456789'))
134
+ const result3 = collection.validate(getFormData('NGABCDEFGH'))
135
+
136
+ expect(result1.errors).toBeTruthy()
137
+ expect(result2.errors).toBeTruthy()
138
+ expect(result3.errors).toBeTruthy()
139
+ })
140
+ })
141
+
142
+ describe('State', () => {
143
+ it('returns text from state', () => {
144
+ const state1 = getFormState('NG12345678')
145
+ const state2 = getFormState(null)
146
+
147
+ const answer1 = getAnswer(field, state1)
148
+ const answer2 = getAnswer(field, state2)
149
+
150
+ expect(answer1).toBe('NG12345678')
151
+ expect(answer2).toBe('')
152
+ })
153
+
154
+ it('returns payload from state', () => {
155
+ const state1 = getFormState('NG12345678')
156
+ const state2 = getFormState(null)
157
+
158
+ const payload1 = field.getFormDataFromState(state1)
159
+ const payload2 = field.getFormDataFromState(state2)
160
+
161
+ expect(payload1).toEqual(getFormData('NG12345678'))
162
+ expect(payload2).toEqual(getFormData())
163
+ })
164
+
165
+ it('returns value from state', () => {
166
+ const state1 = getFormState('NG12345678')
167
+ const state2 = getFormState(null)
168
+
169
+ const value1 = field.getFormValueFromState(state1)
170
+ const value2 = field.getFormValueFromState(state2)
171
+
172
+ expect(value1).toBe('NG12345678')
173
+ expect(value2).toBeUndefined()
174
+ })
175
+
176
+ it('returns context for conditions and form submission', () => {
177
+ const state1 = getFormState('NG12345678')
178
+ const state2 = getFormState(null)
179
+
180
+ const value1 = field.getContextValueFromState(state1)
181
+ const value2 = field.getContextValueFromState(state2)
182
+
183
+ expect(value1).toBe('NG12345678')
184
+ expect(value2).toBeNull()
185
+ })
186
+
187
+ it('returns state from payload', () => {
188
+ const payload1 = getFormData('NG12345678')
189
+ const payload2 = getFormData()
190
+
191
+ const value1 = field.getStateFromValidForm(payload1)
192
+ const value2 = field.getStateFromValidForm(payload2)
193
+
194
+ expect(value1).toEqual(getFormState('NG12345678'))
195
+ expect(value2).toEqual(getFormState(null))
196
+ })
197
+ })
198
+
199
+ describe('View model', () => {
200
+ it('sets Nunjucks component defaults', () => {
201
+ const viewModel = field.getViewModel(getFormData('NG12345678'))
202
+
203
+ expect(viewModel).toEqual(
204
+ expect.objectContaining({
205
+ label: { text: def.title },
206
+ name: 'myComponent',
207
+ id: 'myComponent',
208
+ value: 'NG12345678'
209
+ })
210
+ )
211
+ })
212
+
213
+ it('includes instruction text when provided', () => {
214
+ const componentWithInstruction = new NationalGridFieldNumberField(
215
+ {
216
+ ...def,
217
+ options: { instructionText: 'Enter in format **NG12345678**' }
218
+ },
219
+ { model }
220
+ )
221
+
222
+ const viewModel = componentWithInstruction.getViewModel(
223
+ getFormData('NG12345678')
224
+ )
225
+
226
+ const instructionText =
227
+ 'instructionText' in viewModel ? viewModel.instructionText : undefined
228
+ expect(instructionText).toBeTruthy()
229
+ expect(instructionText).toContain('NG12345678')
230
+ })
231
+ })
232
+
233
+ describe('AllPossibleErrors', () => {
234
+ it('should return errors from instance method', () => {
235
+ const errors = field.getAllPossibleErrors()
236
+ expect(errors.baseErrors).not.toBeEmpty()
237
+ expect(errors.advancedSettingsErrors).toEqual([])
238
+ })
239
+
240
+ it('should return errors from static method', () => {
241
+ const staticErrors = NationalGridFieldNumberField.getAllPossibleErrors()
242
+ expect(staticErrors.baseErrors).not.toBeEmpty()
243
+ expect(staticErrors.advancedSettingsErrors).toEqual([])
244
+ })
245
+ })
246
+ })
247
+
248
+ describe('Validation', () => {
249
+ describe.each([
250
+ {
251
+ description: 'Trim empty spaces',
252
+ component: {
253
+ title: 'Example National Grid field number',
254
+ name: 'myComponent',
255
+ type: ComponentType.NationalGridFieldNumberField,
256
+ options: {}
257
+ },
258
+ assertions: [
259
+ {
260
+ input: getFormData(' NG12345678'),
261
+ output: { value: getFormData('NG 1234 5678') }
262
+ },
263
+ {
264
+ input: getFormData('NG12345678 '),
265
+ output: { value: getFormData('NG 1234 5678') }
266
+ },
267
+ {
268
+ input: getFormData(' NG12345678 \n\n'),
269
+ output: { value: getFormData('NG 1234 5678') }
270
+ }
271
+ ]
272
+ },
273
+ {
274
+ description: 'Pattern validation',
275
+ component: {
276
+ title: 'Example National Grid field number',
277
+ name: 'myComponent',
278
+ type: ComponentType.NationalGridFieldNumberField,
279
+ options: {}
280
+ },
281
+ assertions: [
282
+ {
283
+ input: getFormData('NG1234567'),
284
+ output: {
285
+ value: getFormData('NG1234567'),
286
+ errors: expect.arrayContaining([
287
+ expect.objectContaining({
288
+ text: 'Enter a valid National Grid field number for Example National Grid field number like NG 1234 5678'
289
+ })
290
+ ])
291
+ }
292
+ },
293
+ {
294
+ input: getFormData('N123456789'),
295
+ output: {
296
+ value: getFormData('N123456789'),
297
+ errors: expect.arrayContaining([
298
+ expect.objectContaining({
299
+ text: 'Enter a valid National Grid field number for Example National Grid field number like NG 1234 5678'
300
+ })
301
+ ])
302
+ }
303
+ },
304
+ {
305
+ input: getFormData('NGABCDEFGH'),
306
+ output: {
307
+ value: getFormData('NGABCDEFGH'),
308
+ errors: expect.arrayContaining([
309
+ expect.objectContaining({
310
+ text: 'Enter a valid National Grid field number for Example National Grid field number like NG 1234 5678'
311
+ })
312
+ ])
313
+ }
314
+ }
315
+ ]
316
+ },
317
+ {
318
+ description: 'Custom validation message',
319
+ component: {
320
+ title: 'Example National Grid field number',
321
+ name: 'myComponent',
322
+ type: ComponentType.NationalGridFieldNumberField,
323
+ options: {
324
+ customValidationMessage: 'This is a custom error'
325
+ }
326
+ },
327
+ assertions: [
328
+ {
329
+ input: getFormData(''),
330
+ output: {
331
+ value: getFormData(''),
332
+ errors: [
333
+ expect.objectContaining({
334
+ text: 'This is a custom error'
335
+ })
336
+ ]
337
+ }
338
+ },
339
+ {
340
+ input: getFormData('INVALID'),
341
+ output: {
342
+ value: getFormData('INVALID'),
343
+ errors: expect.arrayContaining([
344
+ expect.objectContaining({
345
+ text: 'This is a custom error'
346
+ })
347
+ ])
348
+ }
349
+ }
350
+ ]
351
+ },
352
+ {
353
+ description: 'Custom validation messages (multiple)',
354
+ component: {
355
+ title: 'Example National Grid field number',
356
+ name: 'myComponent',
357
+ type: ComponentType.NationalGridFieldNumberField,
358
+ options: {
359
+ customValidationMessages: {
360
+ 'any.required': 'This is a custom required error',
361
+ 'string.empty': 'This is a custom empty string error',
362
+ 'string.pattern.base': 'This is a custom pattern error'
363
+ }
364
+ }
365
+ },
366
+ assertions: [
367
+ {
368
+ input: getFormData(),
369
+ output: {
370
+ value: getFormData(''),
371
+ errors: [
372
+ expect.objectContaining({
373
+ text: 'This is a custom required error'
374
+ })
375
+ ]
376
+ }
377
+ },
378
+ {
379
+ input: getFormData(''),
380
+ output: {
381
+ value: getFormData(''),
382
+ errors: [
383
+ expect.objectContaining({
384
+ text: 'This is a custom empty string error'
385
+ })
386
+ ]
387
+ }
388
+ },
389
+ {
390
+ input: getFormData('INVALID'),
391
+ output: {
392
+ value: getFormData('INVALID'),
393
+ errors: expect.arrayContaining([
394
+ expect.objectContaining({
395
+ text: 'This is a custom pattern error'
396
+ })
397
+ ])
398
+ }
399
+ }
400
+ ]
401
+ },
402
+ {
403
+ description: 'Optional field',
404
+ component: {
405
+ title: 'Example National Grid field number',
406
+ name: 'myComponent',
407
+ type: ComponentType.NationalGridFieldNumberField,
408
+ options: {
409
+ required: false
410
+ }
411
+ },
412
+ assertions: [
413
+ {
414
+ input: getFormData(''),
415
+ output: { value: getFormData('') }
416
+ }
417
+ ]
418
+ }
419
+ ])('$description', ({ component: def, assertions }) => {
420
+ let collection: ComponentCollection
421
+
422
+ beforeEach(() => {
423
+ collection = new ComponentCollection(
424
+ [def as NationalGridFieldNumberFieldComponent],
425
+ { model }
426
+ )
427
+ })
428
+
429
+ it.each([...assertions])(
430
+ 'validates custom example',
431
+ ({ input, output }) => {
432
+ const result = collection.validate(input)
433
+ expect(result).toEqual(output)
434
+ }
435
+ )
436
+ })
437
+ })
438
+ })
@@ -0,0 +1,52 @@
1
+ import { type NationalGridFieldNumberFieldComponent } from '@defra/forms-model'
2
+ import type joi from 'joi'
3
+
4
+ import { LocationFieldBase } from '~/src/server/plugins/engine/components/LocationFieldBase.js'
5
+
6
+ export class NationalGridFieldNumberField extends LocationFieldBase {
7
+ declare options: NationalGridFieldNumberFieldComponent['options']
8
+
9
+ protected getValidationConfig() {
10
+ return {
11
+ // Pattern allows spaces and commas in the input since custom validation will clean them
12
+ pattern: /^[A-Z]{2}[\d\s,]*$/i,
13
+ patternErrorMessage: `Enter a valid National Grid field number for ${this.title} like NG 1234 5678`,
14
+ customValidation: (value: string, helpers: joi.CustomHelpers) => {
15
+ // Strip spaces and commas for validation
16
+ const cleanValue = value.replace(/[\s,]/g, '')
17
+
18
+ // Check if it matches the exact pattern after cleaning
19
+ if (!/^[A-Z]{2}\d{8}$/i.test(cleanValue)) {
20
+ return helpers.error('string.pattern.base')
21
+ }
22
+
23
+ // Format with spaces per GDS guidance: NG 1234 5678
24
+ const letters = cleanValue.substring(0, 2)
25
+ const numbers = cleanValue.substring(2)
26
+ const formattedValue = `${letters} ${numbers.substring(0, 4)} ${numbers.substring(4)}`
27
+
28
+ return formattedValue
29
+ }
30
+ }
31
+ }
32
+
33
+ protected getErrorTemplates() {
34
+ return [
35
+ {
36
+ type: 'pattern',
37
+ template:
38
+ 'Enter a valid National Grid field number for [short description] like NG 1234 5678'
39
+ }
40
+ ]
41
+ }
42
+
43
+ /**
44
+ * Static version of getAllPossibleErrors that doesn't require a component instance.
45
+ */
46
+ static getAllPossibleErrors() {
47
+ const instance = Object.create(
48
+ NationalGridFieldNumberField.prototype
49
+ ) as NationalGridFieldNumberField
50
+ return instance.getAllPossibleErrors()
51
+ }
52
+ }