@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,665 @@
1
+ import {
2
+ ComponentType,
3
+ type EastingNorthingFieldComponent
4
+ } from '@defra/forms-model'
5
+
6
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
7
+ import { EastingNorthingField } from '~/src/server/plugins/engine/components/EastingNorthingField.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
+
15
+ describe('EastingNorthingField', () => {
16
+ let model: FormModel
17
+
18
+ beforeEach(() => {
19
+ model = new FormModel(definition, {
20
+ basePath: 'test'
21
+ })
22
+ })
23
+
24
+ describe('Defaults', () => {
25
+ let def: EastingNorthingFieldComponent
26
+ let collection: ComponentCollection
27
+ let field: Field
28
+
29
+ beforeEach(() => {
30
+ def = {
31
+ title: 'Example easting northing',
32
+ shortDescription: 'Example location',
33
+ name: 'myComponent',
34
+ type: ComponentType.EastingNorthingField,
35
+ options: {},
36
+ schema: {}
37
+ } satisfies EastingNorthingFieldComponent
38
+
39
+ collection = new ComponentCollection([def], { model })
40
+ field = collection.fields[0]
41
+ })
42
+
43
+ describe('Schema', () => {
44
+ it('uses collection titles as labels', () => {
45
+ const { formSchema } = collection
46
+ const { keys } = formSchema.describe()
47
+
48
+ expect(keys).toHaveProperty(
49
+ 'myComponent__easting',
50
+ expect.objectContaining({
51
+ flags: expect.objectContaining({ label: 'Easting' })
52
+ })
53
+ )
54
+
55
+ expect(keys).toHaveProperty(
56
+ 'myComponent__northing',
57
+ expect.objectContaining({
58
+ flags: expect.objectContaining({ label: 'Northing' })
59
+ })
60
+ )
61
+ })
62
+
63
+ it('uses collection names as keys', () => {
64
+ const { formSchema } = collection
65
+ const { keys } = formSchema.describe()
66
+
67
+ expect(field.keys).toEqual([
68
+ 'myComponent',
69
+ 'myComponent__easting',
70
+ 'myComponent__northing'
71
+ ])
72
+
73
+ expect(field.collection?.keys).not.toHaveProperty('myComponent')
74
+
75
+ for (const key of field.collection?.keys ?? []) {
76
+ expect(keys).toHaveProperty(key)
77
+ }
78
+ })
79
+
80
+ it('is required by default', () => {
81
+ const { formSchema } = collection
82
+ const { keys } = formSchema.describe()
83
+
84
+ expect(keys).toHaveProperty(
85
+ 'myComponent__easting',
86
+ expect.objectContaining({
87
+ flags: expect.objectContaining({ presence: 'required' })
88
+ })
89
+ )
90
+
91
+ expect(keys).toHaveProperty(
92
+ 'myComponent__northing',
93
+ expect.objectContaining({
94
+ flags: expect.objectContaining({ presence: 'required' })
95
+ })
96
+ )
97
+ })
98
+
99
+ it('is optional when configured', () => {
100
+ const collectionOptional = new ComponentCollection(
101
+ [
102
+ {
103
+ title: 'Example easting northing',
104
+ name: 'myComponent',
105
+ type: ComponentType.EastingNorthingField,
106
+ options: { required: false },
107
+ schema: {}
108
+ }
109
+ ],
110
+ { model }
111
+ )
112
+
113
+ const { formSchema } = collectionOptional
114
+ const { keys } = formSchema.describe()
115
+
116
+ expect(keys).toHaveProperty(
117
+ 'myComponent__easting',
118
+ expect.objectContaining({ allow: [''] })
119
+ )
120
+
121
+ expect(keys).toHaveProperty(
122
+ 'myComponent__northing',
123
+ expect.objectContaining({ allow: [''] })
124
+ )
125
+
126
+ const result1 = collectionOptional.validate(
127
+ getFormData({
128
+ easting: '',
129
+ northing: ''
130
+ })
131
+ )
132
+
133
+ const result2 = collectionOptional.validate(
134
+ getFormData({
135
+ easting: '12345',
136
+ northing: ''
137
+ })
138
+ )
139
+
140
+ expect(result1.errors).toBeUndefined()
141
+ expect(result2.errors).toBeTruthy()
142
+ expect(result2.errors?.length).toBeGreaterThan(0)
143
+ })
144
+
145
+ it('accepts valid values', () => {
146
+ const result1 = collection.validate(
147
+ getFormData({
148
+ easting: '12345',
149
+ northing: '1234567'
150
+ })
151
+ )
152
+
153
+ const result2 = collection.validate(
154
+ getFormData({
155
+ easting: '0',
156
+ northing: '0'
157
+ })
158
+ )
159
+
160
+ expect(result1.errors).toBeUndefined()
161
+ expect(result2.errors).toBeUndefined()
162
+ })
163
+
164
+ it('adds errors for empty value when short description exists', () => {
165
+ const result = collection.validate(
166
+ getFormData({
167
+ easting: '',
168
+ northing: ''
169
+ })
170
+ )
171
+
172
+ expect(result.errors).toBeTruthy()
173
+ expect(result.errors?.length).toBe(2)
174
+ })
175
+
176
+ it('adds errors for invalid values', () => {
177
+ const result1 = collection.validate(
178
+ getFormData({
179
+ easting: 'invalid',
180
+ northing: 'invalid'
181
+ })
182
+ )
183
+
184
+ const result2 = collection.validate(
185
+ getFormData({
186
+ easting: '12345.5',
187
+ northing: '1234567.5'
188
+ })
189
+ )
190
+
191
+ expect(result1.errors).toBeTruthy()
192
+ expect(result2.errors).toBeTruthy()
193
+ })
194
+ })
195
+
196
+ describe('State', () => {
197
+ it('returns text from state', () => {
198
+ const state1 = getFormState({
199
+ easting: 12345,
200
+ northing: 1234567
201
+ })
202
+ const state2 = getFormState({})
203
+
204
+ const answer1 = getAnswer(field, state1)
205
+ const answer2 = getAnswer(field, state2)
206
+
207
+ expect(answer1).toBe('Northing: 1234567<br>Easting: 12345<br>')
208
+ expect(answer2).toBe('')
209
+ })
210
+
211
+ it('returns payload from state', () => {
212
+ const state1 = getFormState({
213
+ easting: 12345,
214
+ northing: 1234567
215
+ })
216
+ const state2 = getFormState({})
217
+
218
+ const payload1 = field.getFormDataFromState(state1)
219
+ const payload2 = field.getFormDataFromState(state2)
220
+
221
+ expect(payload1).toEqual(
222
+ getFormData({
223
+ easting: 12345,
224
+ northing: 1234567
225
+ })
226
+ )
227
+ expect(payload2).toEqual(getFormData({}))
228
+ })
229
+
230
+ it('returns value from state', () => {
231
+ const state1 = getFormState({
232
+ easting: 12345,
233
+ northing: 1234567
234
+ })
235
+ const state2 = getFormState({})
236
+
237
+ const value1 = field.getFormValueFromState(state1)
238
+ const value2 = field.getFormValueFromState(state2)
239
+
240
+ expect(value1).toEqual({
241
+ easting: 12345,
242
+ northing: 1234567
243
+ })
244
+
245
+ expect(value2).toBeUndefined()
246
+ })
247
+
248
+ it('returns context for conditions and form submission', () => {
249
+ const state1 = getFormState({
250
+ easting: 12345,
251
+ northing: 1234567
252
+ })
253
+ const state2 = getFormState({})
254
+
255
+ const value1 = field.getContextValueFromState(state1)
256
+ const value2 = field.getContextValueFromState(state2)
257
+
258
+ expect(value1).toBe('Northing: 1234567\nEasting: 12345')
259
+ expect(value2).toBeNull()
260
+ })
261
+
262
+ it('returns state from payload', () => {
263
+ const payload1 = getFormData({
264
+ easting: 12345,
265
+ northing: 1234567
266
+ })
267
+ const payload2 = getFormData({})
268
+
269
+ const value1 = field.getStateFromValidForm(payload1)
270
+ const value2 = field.getStateFromValidForm(payload2)
271
+
272
+ expect(value1).toEqual(
273
+ getFormState({
274
+ easting: 12345,
275
+ northing: 1234567
276
+ })
277
+ )
278
+ expect(value2).toEqual(getFormState({}))
279
+ })
280
+ })
281
+
282
+ describe('View model', () => {
283
+ it('sets Nunjucks component defaults', () => {
284
+ const payload = getFormData({
285
+ easting: 12345,
286
+ northing: 1234567
287
+ })
288
+ const viewModel = field.getViewModel(payload)
289
+
290
+ expect(viewModel).toEqual(
291
+ expect.objectContaining({
292
+ fieldset: {
293
+ legend: {
294
+ text: def.title,
295
+ classes: 'govuk-fieldset__legend--m'
296
+ }
297
+ },
298
+ items: [
299
+ expect.objectContaining({
300
+ label: expect.objectContaining({ text: 'Easting' }),
301
+ name: 'myComponent__easting',
302
+ id: 'myComponent__easting',
303
+ value: 12345
304
+ }),
305
+ expect.objectContaining({
306
+ label: expect.objectContaining({ text: 'Northing' }),
307
+ name: 'myComponent__northing',
308
+ id: 'myComponent__northing',
309
+ value: 1234567
310
+ })
311
+ ]
312
+ })
313
+ )
314
+ })
315
+
316
+ it('includes instruction text when provided', () => {
317
+ const componentWithInstruction = new EastingNorthingField(
318
+ {
319
+ ...def,
320
+ options: { instructionText: 'Enter coordinates in **meters**' }
321
+ },
322
+ { model }
323
+ )
324
+
325
+ const viewModel = componentWithInstruction.getViewModel(
326
+ getFormData({
327
+ easting: 12345,
328
+ northing: 1234567
329
+ })
330
+ )
331
+
332
+ const instructionText =
333
+ 'instructionText' in viewModel ? viewModel.instructionText : undefined
334
+ expect(instructionText).toBeTruthy()
335
+ expect(instructionText).toContain('meters')
336
+ })
337
+
338
+ it('sets error classes when component has errors', () => {
339
+ const payload = getFormData({
340
+ easting: '',
341
+ northing: ''
342
+ })
343
+
344
+ const errors = [
345
+ {
346
+ name: 'myComponent',
347
+ text: 'Error message',
348
+ path: ['myComponent'],
349
+ href: '#myComponent'
350
+ }
351
+ ]
352
+
353
+ const viewModel = field.getViewModel(payload, errors)
354
+
355
+ expect(viewModel.items?.[0]).toEqual(
356
+ expect.objectContaining({
357
+ classes: expect.stringContaining('govuk-input--error')
358
+ })
359
+ )
360
+
361
+ expect(viewModel.items?.[1]).toEqual(
362
+ expect.objectContaining({
363
+ classes: expect.stringContaining('govuk-input--error')
364
+ })
365
+ )
366
+ })
367
+ })
368
+
369
+ describe('AllPossibleErrors', () => {
370
+ it('should return errors from instance method', () => {
371
+ const errors = field.getAllPossibleErrors()
372
+ expect(errors.baseErrors).not.toBeEmpty()
373
+ expect(errors.advancedSettingsErrors).not.toBeEmpty()
374
+ })
375
+
376
+ it('should return errors from static method', () => {
377
+ const staticErrors = EastingNorthingField.getAllPossibleErrors()
378
+ expect(staticErrors.baseErrors).not.toBeEmpty()
379
+ expect(staticErrors.advancedSettingsErrors).not.toBeEmpty()
380
+ })
381
+
382
+ it('instance method should delegate to static method', () => {
383
+ const staticResult = EastingNorthingField.getAllPossibleErrors()
384
+ const instanceResult = field.getAllPossibleErrors()
385
+
386
+ expect(instanceResult).toEqual(staticResult)
387
+ })
388
+ })
389
+ })
390
+
391
+ describe('Validation', () => {
392
+ describe.each([
393
+ {
394
+ description: 'Trim empty spaces',
395
+ component: {
396
+ title: 'Example easting northing',
397
+ name: 'myComponent',
398
+ type: ComponentType.EastingNorthingField,
399
+ options: {},
400
+ schema: {}
401
+ } satisfies EastingNorthingFieldComponent,
402
+ assertions: [
403
+ {
404
+ input: getFormData({
405
+ easting: ' 12345',
406
+ northing: ' 1234567'
407
+ }),
408
+ output: {
409
+ value: getFormData({
410
+ easting: 12345,
411
+ northing: 1234567
412
+ })
413
+ }
414
+ },
415
+ {
416
+ input: getFormData({
417
+ easting: '12345 ',
418
+ northing: '1234567 '
419
+ }),
420
+ output: {
421
+ value: getFormData({
422
+ easting: 12345,
423
+ northing: 1234567
424
+ })
425
+ }
426
+ }
427
+ ]
428
+ },
429
+ {
430
+ description: 'Schema min and max for easting',
431
+ component: {
432
+ title: 'Example easting northing',
433
+ name: 'myComponent',
434
+ type: ComponentType.EastingNorthingField,
435
+ options: {},
436
+ schema: {
437
+ easting: {
438
+ min: 1000,
439
+ max: 60000
440
+ }
441
+ }
442
+ } satisfies EastingNorthingFieldComponent,
443
+ assertions: [
444
+ {
445
+ input: getFormData({
446
+ easting: '999',
447
+ northing: '1234567'
448
+ }),
449
+ output: {
450
+ value: getFormData({
451
+ easting: 999,
452
+ northing: 1234567
453
+ }),
454
+ errors: [
455
+ expect.objectContaining({
456
+ text: expect.stringMatching(
457
+ /Easting for .* must be between 1000 and 60000/
458
+ )
459
+ })
460
+ ]
461
+ }
462
+ },
463
+ {
464
+ input: getFormData({
465
+ easting: '60001',
466
+ northing: '1234567'
467
+ }),
468
+ output: {
469
+ value: getFormData({
470
+ easting: 60001,
471
+ northing: 1234567
472
+ }),
473
+ errors: [
474
+ expect.objectContaining({
475
+ text: expect.stringMatching(
476
+ /Easting for .* must be between 1000 and 60000/
477
+ )
478
+ })
479
+ ]
480
+ }
481
+ }
482
+ ]
483
+ },
484
+ {
485
+ description: 'Schema min and max for northing',
486
+ component: {
487
+ title: 'Example easting northing',
488
+ name: 'myComponent',
489
+ type: ComponentType.EastingNorthingField,
490
+ options: {},
491
+ schema: {
492
+ northing: {
493
+ min: 1000,
494
+ max: 1200000
495
+ }
496
+ }
497
+ } satisfies EastingNorthingFieldComponent,
498
+ assertions: [
499
+ {
500
+ input: getFormData({
501
+ easting: '12345',
502
+ northing: '999'
503
+ }),
504
+ output: {
505
+ value: getFormData({
506
+ easting: 12345,
507
+ northing: 999
508
+ }),
509
+ errors: [
510
+ expect.objectContaining({
511
+ text: expect.stringMatching(
512
+ /Northing for .* must be between 1000 and 1200000/
513
+ )
514
+ })
515
+ ]
516
+ }
517
+ },
518
+ {
519
+ input: getFormData({
520
+ easting: '12345',
521
+ northing: '1200001'
522
+ }),
523
+ output: {
524
+ value: getFormData({
525
+ easting: 12345,
526
+ northing: 1200001
527
+ }),
528
+ errors: [
529
+ expect.objectContaining({
530
+ text: expect.stringMatching(
531
+ /Northing for .* must be between 1000 and 1200000/
532
+ )
533
+ })
534
+ ]
535
+ }
536
+ }
537
+ ]
538
+ },
539
+ {
540
+ description: 'Precision validation',
541
+ component: {
542
+ title: 'Example easting northing',
543
+ name: 'myComponent',
544
+ type: ComponentType.EastingNorthingField,
545
+ options: {},
546
+ schema: {}
547
+ } satisfies EastingNorthingFieldComponent,
548
+ assertions: [
549
+ {
550
+ input: getFormData({
551
+ easting: '12345.5',
552
+ northing: '1234567'
553
+ }),
554
+ output: {
555
+ value: getFormData({
556
+ easting: 12345.5,
557
+ northing: 1234567
558
+ }),
559
+ errors: [
560
+ expect.objectContaining({
561
+ text: expect.stringMatching(
562
+ /Easting for .* must be between 1 and 5 digits/
563
+ )
564
+ })
565
+ ]
566
+ }
567
+ },
568
+ {
569
+ input: getFormData({
570
+ easting: '12345',
571
+ northing: '1234567.5'
572
+ }),
573
+ output: {
574
+ value: getFormData({
575
+ easting: 12345,
576
+ northing: 1234567.5
577
+ }),
578
+ errors: [
579
+ expect.objectContaining({
580
+ text: expect.stringMatching(
581
+ /Northing for .* must be between 1 and 7 digits/
582
+ )
583
+ })
584
+ ]
585
+ }
586
+ }
587
+ ]
588
+ },
589
+ {
590
+ description: 'Optional field',
591
+ component: {
592
+ title: 'Example easting northing',
593
+ name: 'myComponent',
594
+ type: ComponentType.EastingNorthingField,
595
+ options: {
596
+ required: false
597
+ },
598
+ schema: {}
599
+ } satisfies EastingNorthingFieldComponent,
600
+ assertions: [
601
+ {
602
+ input: getFormData({
603
+ easting: '',
604
+ northing: ''
605
+ }),
606
+ output: {
607
+ value: getFormData({
608
+ easting: '',
609
+ northing: ''
610
+ })
611
+ }
612
+ }
613
+ ]
614
+ }
615
+ ])('$description', ({ component: def, assertions }) => {
616
+ let collection: ComponentCollection
617
+
618
+ beforeEach(() => {
619
+ collection = new ComponentCollection([def], { model })
620
+ })
621
+
622
+ it.each([...assertions])(
623
+ 'validates custom example',
624
+ ({ input, output }) => {
625
+ const result = collection.validate(input)
626
+ expect(result).toEqual(output)
627
+ }
628
+ )
629
+ })
630
+ })
631
+ })
632
+
633
+ function getFormData(
634
+ value:
635
+ | { easting?: string | number; northing?: string | number }
636
+ | Record<string, never>
637
+ ) {
638
+ if ('easting' in value || 'northing' in value) {
639
+ return {
640
+ myComponent__easting: value.easting,
641
+ myComponent__northing: value.northing
642
+ }
643
+ }
644
+ return {}
645
+ }
646
+
647
+ function getFormState(
648
+ value:
649
+ | {
650
+ easting?: number
651
+ northing?: number
652
+ }
653
+ | Record<string, never>
654
+ ) {
655
+ if ('easting' in value || 'northing' in value) {
656
+ return {
657
+ myComponent__easting: value.easting ?? null,
658
+ myComponent__northing: value.northing ?? null
659
+ }
660
+ }
661
+ return {
662
+ myComponent__easting: null,
663
+ myComponent__northing: null
664
+ }
665
+ }