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