@defra/forms-engine-plugin 4.0.10 → 4.0.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 (35) hide show
  1. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +2 -2
  2. package/.server/server/plugins/engine/components/EastingNorthingField.js +4 -24
  3. package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -1
  4. package/.server/server/plugins/engine/components/LatLongField.js +8 -31
  5. package/.server/server/plugins/engine/components/LatLongField.js.map +1 -1
  6. package/.server/server/plugins/engine/components/LocationFieldBase.d.ts +1 -3
  7. package/.server/server/plugins/engine/components/LocationFieldBase.js +1 -8
  8. package/.server/server/plugins/engine/components/LocationFieldBase.js.map +1 -1
  9. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.d.ts +0 -2
  10. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js +7 -18
  11. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js.map +1 -1
  12. package/.server/server/plugins/engine/components/NumberField.d.ts +0 -18
  13. package/.server/server/plugins/engine/components/NumberField.js +2 -103
  14. package/.server/server/plugins/engine/components/NumberField.js.map +1 -1
  15. package/.server/server/plugins/engine/components/OsGridRefField.d.ts +0 -2
  16. package/.server/server/plugins/engine/components/OsGridRefField.js +4 -32
  17. package/.server/server/plugins/engine/components/OsGridRefField.js.map +1 -1
  18. package/.server/server/plugins/engine/pageControllers/helpers/pages.d.ts +1 -2
  19. package/.server/server/plugins/engine/pageControllers/helpers/pages.js +1 -11
  20. package/.server/server/plugins/engine/pageControllers/helpers/pages.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/server/forms/register-as-a-unicorn-breeder.yaml +2 -2
  23. package/src/server/plugins/engine/components/EastingNorthingField.test.ts +0 -14
  24. package/src/server/plugins/engine/components/EastingNorthingField.ts +4 -24
  25. package/src/server/plugins/engine/components/LatLongField.test.ts +4 -24
  26. package/src/server/plugins/engine/components/LatLongField.ts +8 -33
  27. package/src/server/plugins/engine/components/LocationFieldBase.ts +1 -15
  28. package/src/server/plugins/engine/components/NationalGridFieldNumberField.test.ts +22 -8
  29. package/src/server/plugins/engine/components/NationalGridFieldNumberField.ts +9 -20
  30. package/src/server/plugins/engine/components/NumberField.test.ts +12 -504
  31. package/src/server/plugins/engine/components/NumberField.ts +4 -133
  32. package/src/server/plugins/engine/components/OsGridRefField.test.ts +11 -33
  33. package/src/server/plugins/engine/components/OsGridRefField.ts +5 -38
  34. package/src/server/plugins/engine/pageControllers/helpers/helpers.test.ts +4 -24
  35. package/src/server/plugins/engine/pageControllers/helpers/pages.ts +0 -15
@@ -1,9 +1,5 @@
1
1
  import { type NumberFieldComponent } from '@defra/forms-model'
2
- import joi, {
3
- type CustomHelpers,
4
- type CustomValidator,
5
- type NumberSchema
6
- } from 'joi'
2
+ import joi, { type CustomValidator, type NumberSchema } from 'joi'
7
3
 
8
4
  import {
9
5
  FormComponent,
@@ -165,154 +161,29 @@ export class NumberField extends FormComponent {
165
161
  }
166
162
  }
167
163
 
168
- /**
169
- * Validates string length of a numeric value
170
- * @param value - The numeric value to validate
171
- * @param minLength - Minimum required string length
172
- * @param maxLength - Maximum allowed string length
173
- * @returns Object with validation result
174
- */
175
- export function validateStringLength(
176
- value: number,
177
- minLength?: number,
178
- maxLength?: number
179
- ): { isValid: boolean; error?: 'minLength' | 'maxLength' } {
180
- if (typeof minLength !== 'number' && typeof maxLength !== 'number') {
181
- return { isValid: true }
182
- }
183
-
184
- const valueStr = String(value)
185
-
186
- if (typeof minLength === 'number' && valueStr.length < minLength) {
187
- return { isValid: false, error: 'minLength' }
188
- }
189
-
190
- if (typeof maxLength === 'number' && valueStr.length > maxLength) {
191
- return { isValid: false, error: 'maxLength' }
192
- }
193
-
194
- return { isValid: true }
195
- }
196
-
197
- /**
198
- * Validates minimum decimal precision
199
- * @param value - The numeric value to validate
200
- * @param minPrecision - Minimum required decimal places
201
- * @returns true if valid, false if invalid
202
- */
203
- export function validateMinimumPrecision(
204
- value: number,
205
- minPrecision: number
206
- ): boolean {
207
- if (Number.isInteger(value)) {
208
- return false
209
- }
210
-
211
- const valueStr = String(value)
212
- const decimalIndex = valueStr.indexOf('.')
213
-
214
- if (decimalIndex !== -1) {
215
- const decimalPlaces = valueStr.length - decimalIndex - 1
216
- return decimalPlaces >= minPrecision
217
- }
218
-
219
- return false
220
- }
221
-
222
- /**
223
- * Helper function to handle length validation errors
224
- * Returns the appropriate error response based on the validation result
225
- */
226
- function handleLengthValidationError(
227
- lengthCheck: ReturnType<typeof validateStringLength>,
228
- helpers: CustomHelpers,
229
- custom: string | undefined,
230
- minLength: number | undefined,
231
- maxLength: number | undefined
232
- ) {
233
- if (!lengthCheck.isValid && lengthCheck.error) {
234
- const errorType = `number.${lengthCheck.error}`
235
-
236
- if (custom) {
237
- // Only pass the relevant length value in context
238
- const contextData =
239
- lengthCheck.error === 'minLength'
240
- ? { minLength: minLength ?? 0 }
241
- : { maxLength: maxLength ?? 0 }
242
- return helpers.message({ custom }, contextData)
243
- }
244
-
245
- const context =
246
- lengthCheck.error === 'minLength'
247
- ? { minLength: minLength ?? 0 }
248
- : { maxLength: maxLength ?? 0 }
249
- return helpers.error(errorType, context)
250
- }
251
- return null
252
- }
253
-
254
164
  export function getValidatorPrecision(component: NumberField) {
255
165
  const validator: CustomValidator = (value: number, helpers) => {
256
166
  const { options, schema } = component
167
+
257
168
  const { customValidationMessage: custom } = options
258
- const {
259
- precision: limit,
260
- minPrecision,
261
- minLength,
262
- maxLength
263
- } = schema as {
264
- precision?: number
265
- minPrecision?: number
266
- minLength?: number
267
- maxLength?: number
268
- }
169
+ const { precision: limit } = schema
269
170
 
270
171
  if (!limit || limit <= 0) {
271
- const lengthCheck = validateStringLength(value, minLength, maxLength)
272
- const error = handleLengthValidationError(
273
- lengthCheck,
274
- helpers,
275
- custom,
276
- minLength,
277
- maxLength
278
- )
279
- if (error) return error
280
172
  return value
281
173
  }
282
174
 
283
- // Validate precision (max decimal places)
284
175
  const validationSchema = joi
285
176
  .number()
286
177
  .precision(limit)
287
178
  .prefs({ convert: false })
288
179
 
289
180
  try {
290
- joi.attempt(value, validationSchema)
181
+ return joi.attempt(value, validationSchema)
291
182
  } catch {
292
183
  return custom
293
184
  ? helpers.message({ custom }, { limit })
294
185
  : helpers.error('number.precision', { limit })
295
186
  }
296
-
297
- // Validate minimum precision (min decimal places)
298
- if (typeof minPrecision === 'number' && minPrecision > 0) {
299
- if (!validateMinimumPrecision(value, minPrecision)) {
300
- return helpers.error('number.minPrecision', { minPrecision })
301
- }
302
- }
303
-
304
- // Check string length validation after precision checks
305
- const lengthCheck = validateStringLength(value, minLength, maxLength)
306
- const error = handleLengthValidationError(
307
- lengthCheck,
308
- helpers,
309
- custom,
310
- minLength,
311
- maxLength
312
- )
313
- if (error) return error
314
-
315
- return value
316
187
  }
317
188
 
318
189
  return validator
@@ -100,46 +100,24 @@ describe('OsGridRefField', () => {
100
100
  const result1 = collection.validate(getFormData('SD865005'))
101
101
  const result2 = collection.validate(getFormData('SD 865 005'))
102
102
 
103
- // Test 8-digit parcel ID format (2x4)
104
- const result3 = collection.validate(getFormData('TQ12345678'))
105
- const result4 = collection.validate(getFormData('TQ 1234 5678'))
106
-
107
- // Test 10-digit OS grid reference format (2x5)
108
- const result5 = collection.validate(getFormData('SU1234567890'))
109
- const result6 = collection.validate(getFormData('SU 12345 67890'))
110
-
111
103
  // Test case-insensitive
112
- const result7 = collection.validate(getFormData('nt12345678'))
113
-
114
- // Test various valid OS grid formats
115
- const result8 = collection.validate(getFormData('SN 1232 1223')) // parcel ID format
116
- const result9 = collection.validate(getFormData('SN 12324 12234')) // OS grid ref format
117
- const result10 = collection.validate(getFormData('ST 6789 6789')) // parcel ID with different letters
118
- const result11 = collection.validate(getFormData('SO 12345 12345')) // OS grid ref with different letters
104
+ const result3 = collection.validate(getFormData('nt123456'))
119
105
 
120
106
  expect(result1.errors).toBeUndefined()
121
107
  expect(result2.errors).toBeUndefined()
122
108
  expect(result3.errors).toBeUndefined()
123
- expect(result4.errors).toBeUndefined()
124
- expect(result5.errors).toBeUndefined()
125
- expect(result6.errors).toBeUndefined()
126
- expect(result7.errors).toBeUndefined()
127
- expect(result8.errors).toBeUndefined()
128
- expect(result9.errors).toBeUndefined()
129
- expect(result10.errors).toBeUndefined()
130
- expect(result11.errors).toBeUndefined()
131
109
  })
132
110
 
133
- it('formats values with spaces per GDS guidance', () => {
111
+ it('retains values with spaces per GDS guidance', () => {
134
112
  const result1 = collection.validate(getFormData('SD865005'))
135
113
  const result2 = collection.validate(getFormData('TQ 1234 5678'))
136
114
  const result3 = collection.validate(getFormData('SU1234567890'))
137
115
  const result4 = collection.validate(getFormData('TQ12345678'))
138
116
 
139
- expect(result1.value.myComponent).toBe('SD 865 005')
117
+ expect(result1.value.myComponent).toBe('SD865005')
140
118
  expect(result2.value.myComponent).toBe('TQ 1234 5678')
141
- expect(result3.value.myComponent).toBe('SU 12345 67890')
142
- expect(result4.value.myComponent).toBe('TQ 1234 5678')
119
+ expect(result3.value.myComponent).toBe('SU1234567890')
120
+ expect(result4.value.myComponent).toBe('TQ12345678')
143
121
  })
144
122
 
145
123
  it('adds errors for empty value', () => {
@@ -289,16 +267,16 @@ describe('OsGridRefField', () => {
289
267
  },
290
268
  assertions: [
291
269
  {
292
- input: getFormData(' TQ12345678'),
293
- output: { value: getFormData('TQ 1234 5678') }
270
+ input: getFormData(' TQ123456'),
271
+ output: { value: getFormData('TQ123456') }
294
272
  },
295
273
  {
296
- input: getFormData('TQ12345678 '),
297
- output: { value: getFormData('TQ 1234 5678') }
274
+ input: getFormData('TQ123456 '),
275
+ output: { value: getFormData('TQ123456') }
298
276
  },
299
277
  {
300
- input: getFormData(' TQ12345678 \n\n'),
301
- output: { value: getFormData('TQ 1234 5678') }
278
+ input: getFormData(' TQ123456 \n\n'),
279
+ output: { value: getFormData('TQ123456') }
302
280
  }
303
281
  ]
304
282
  },
@@ -1,5 +1,4 @@
1
1
  import { type OsGridRefFieldComponent } from '@defra/forms-model'
2
- import type joi from 'joi'
3
2
 
4
3
  import { LocationFieldBase } from '~/src/server/plugins/engine/components/LocationFieldBase.js'
5
4
 
@@ -9,45 +8,13 @@ export class OsGridRefField extends LocationFieldBase {
9
8
  protected getValidationConfig() {
10
9
  // Regex for OS grid references and parcel IDs
11
10
  // Validates specific valid OS grid letter combinations with:
12
- // - 6 digits (e.g., SD865005 or SD 865 005)
13
- // - 8 digits in 2 blocks of 4 (parcel ID) e.g., ST 6789 6789
14
- // - 10 digits in 2 blocks of 5 (OS grid reference) e.g., SO 12345 12345
15
- const osGridPattern =
16
- /^(?:[sn][a-hj-z]|[to][abfglmqrvw]|h[l-z]|j[lmqrvw])\s?(?:\d{3}\s?\d{3}|\d{4}\s?\d{4}|\d{5}\s?\d{5})$/i
17
-
18
- // More permissive pattern for initial validation (allows spaces to be cleaned)
19
- const initialPattern = /^[A-Za-z]{2}[\d\s]*$/
11
+ // - 2 letters & 6 digits (e.g., SD865005 or SD 865 005)
12
+ const pattern =
13
+ /^((([sS]|[nN])[a-hA-Hj-zJ-Z])|(([tT]|[oO])[abfglmqrvwABFGLMQRVW])|([hH][l-zL-Z])|([jJ][lmqrvwLMQRVW]))\s?(([0-9]{3})\s?([0-9]{3}))$/
20
14
 
21
15
  return {
22
- pattern: initialPattern,
23
- patternErrorMessage: `Enter a valid OS grid reference for ${this.title} like TQ123456`,
24
- customValidation: (value: string, helpers: joi.CustomHelpers) => {
25
- // Strip spaces from the input for processing
26
- const cleanValue = value.replace(/\s/g, '')
27
- const letters = cleanValue.substring(0, 2)
28
- const numbers = cleanValue.substring(2)
29
-
30
- // Validate number length
31
- if (
32
- numbers.length !== 6 &&
33
- numbers.length !== 8 &&
34
- numbers.length !== 10
35
- ) {
36
- return helpers.error('string.pattern.base')
37
- }
38
-
39
- // Format with spaces: XX 123 456, XX 1234 5678, or XX 12345 67890
40
- const halfLength = numbers.length / 2
41
- const formattedValue = `${letters} ${numbers.substring(0, halfLength)} ${numbers.substring(halfLength)}`
42
-
43
- // Validate the formatted value against the OS grid pattern
44
- if (!osGridPattern.test(formattedValue)) {
45
- return helpers.error('string.pattern.base')
46
- }
47
-
48
- // Return formatted value with spaces per GDS guidance
49
- return formattedValue
50
- }
16
+ pattern,
17
+ patternErrorMessage: `Enter a valid OS grid reference for ${this.title} like TQ123456`
51
18
  }
52
19
  }
53
20
 
@@ -11,7 +11,6 @@ import { PageController } from '~/src/server/plugins/engine/pageControllers/Page
11
11
  import { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'
12
12
  import {
13
13
  createPage,
14
- isPageController,
15
14
  type PageControllerType
16
15
  } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
17
16
  import {
@@ -26,7 +25,10 @@ import {
26
25
  import definition from '~/test/form/definitions/blank.js'
27
26
 
28
27
  describe('Page controller helpers', () => {
29
- const examples = PageTypes.map((pageType) => {
28
+ const examples = PageTypes.filter(
29
+ (pageType) =>
30
+ pageType.controller !== ControllerType.SummaryWithConfirmationEmail
31
+ ).map((pageType) => {
30
32
  const pageDef = structuredClone(pageType)
31
33
 
32
34
  let controller: PageControllerType | undefined
@@ -67,10 +69,6 @@ describe('Page controller helpers', () => {
67
69
  controller = SummaryPageController
68
70
  break
69
71
 
70
- case ControllerType.SummaryWithConfirmationEmail:
71
- controller = SummaryPageController
72
- break
73
-
74
72
  case ControllerType.Status:
75
73
  controller = StatusPageController
76
74
  break
@@ -155,24 +153,6 @@ describe('Page controller helpers', () => {
155
153
  })
156
154
  })
157
155
 
158
- describe('Helper: isPageController', () => {
159
- it.each([...examples])(
160
- "allows valid page controller '$pageDef.controller'",
161
- ({ pageDef }) => {
162
- expect(isPageController(pageDef.controller)).toBe(true)
163
- }
164
- )
165
-
166
- it.each([
167
- { name: './pages/unknown.js' },
168
- { name: 'UnknownPageController' },
169
- { name: undefined },
170
- { name: '' }
171
- ])("rejects invalid page controller '$name'", ({ name }) => {
172
- expect(isPageController(name)).toBe(false)
173
- })
174
- })
175
-
176
156
  describe('Helper: getProxyUrlForLocalDevelopment', () => {
177
157
  it('returns null if uploadUrl is undefined', () => {
178
158
  expect(getProxyUrlForLocalDevelopment(undefined)).toBeNull()
@@ -1,23 +1,12 @@
1
1
  import {
2
2
  ControllerType,
3
3
  controllerNameFromPath,
4
- isControllerName,
5
4
  type Page
6
5
  } from '@defra/forms-model'
7
6
 
8
7
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
9
8
  import * as PageControllers from '~/src/server/plugins/engine/pageControllers/index.js'
10
9
 
11
- export function isPageController(
12
- controllerName?: string | ControllerType
13
- ): controllerName is keyof typeof PageControllers {
14
- // Handle SummaryWithConfirmationEmail as it uses SummaryPageController
15
- if (controllerName === ControllerType.SummaryWithConfirmationEmail) {
16
- return true
17
- }
18
- return isControllerName(controllerName) && controllerName in PageControllers
19
- }
20
-
21
10
  export type PageControllerClass = InstanceType<PageControllerType>
22
11
  export type PageControllerType =
23
12
  (typeof PageControllers)[keyof typeof PageControllers]
@@ -56,10 +45,6 @@ export function createPage(model: FormModel, pageDef: Page) {
56
45
  controller = new PageControllers.SummaryPageController(model, pageDef)
57
46
  break
58
47
 
59
- case ControllerType.SummaryWithConfirmationEmail:
60
- controller = new PageControllers.SummaryPageController(model, pageDef)
61
- break
62
-
63
48
  case ControllerType.Status:
64
49
  controller = new PageControllers.StatusPageController(model, pageDef)
65
50
  break