@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.
- package/.server/server/forms/register-as-a-unicorn-breeder.yaml +2 -2
- package/.server/server/plugins/engine/components/EastingNorthingField.js +4 -24
- package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -1
- package/.server/server/plugins/engine/components/LatLongField.js +8 -31
- package/.server/server/plugins/engine/components/LatLongField.js.map +1 -1
- package/.server/server/plugins/engine/components/LocationFieldBase.d.ts +1 -3
- package/.server/server/plugins/engine/components/LocationFieldBase.js +1 -8
- package/.server/server/plugins/engine/components/LocationFieldBase.js.map +1 -1
- package/.server/server/plugins/engine/components/NationalGridFieldNumberField.d.ts +0 -2
- package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js +7 -18
- package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js.map +1 -1
- package/.server/server/plugins/engine/components/NumberField.d.ts +0 -18
- package/.server/server/plugins/engine/components/NumberField.js +2 -103
- package/.server/server/plugins/engine/components/NumberField.js.map +1 -1
- package/.server/server/plugins/engine/components/OsGridRefField.d.ts +0 -2
- package/.server/server/plugins/engine/components/OsGridRefField.js +4 -32
- package/.server/server/plugins/engine/components/OsGridRefField.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/pages.d.ts +1 -2
- package/.server/server/plugins/engine/pageControllers/helpers/pages.js +1 -11
- package/.server/server/plugins/engine/pageControllers/helpers/pages.js.map +1 -1
- package/package.json +1 -1
- package/src/server/forms/register-as-a-unicorn-breeder.yaml +2 -2
- package/src/server/plugins/engine/components/EastingNorthingField.test.ts +0 -14
- package/src/server/plugins/engine/components/EastingNorthingField.ts +4 -24
- package/src/server/plugins/engine/components/LatLongField.test.ts +4 -24
- package/src/server/plugins/engine/components/LatLongField.ts +8 -33
- package/src/server/plugins/engine/components/LocationFieldBase.ts +1 -15
- package/src/server/plugins/engine/components/NationalGridFieldNumberField.test.ts +22 -8
- package/src/server/plugins/engine/components/NationalGridFieldNumberField.ts +9 -20
- package/src/server/plugins/engine/components/NumberField.test.ts +12 -504
- package/src/server/plugins/engine/components/NumberField.ts +4 -133
- package/src/server/plugins/engine/components/OsGridRefField.test.ts +11 -33
- package/src/server/plugins/engine/components/OsGridRefField.ts +5 -38
- package/src/server/plugins/engine/pageControllers/helpers/helpers.test.ts +4 -24
- 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
|
|
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('
|
|
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('
|
|
117
|
+
expect(result1.value.myComponent).toBe('SD865005')
|
|
140
118
|
expect(result2.value.myComponent).toBe('TQ 1234 5678')
|
|
141
|
-
expect(result3.value.myComponent).toBe('
|
|
142
|
-
expect(result4.value.myComponent).toBe('
|
|
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('
|
|
293
|
-
output: { value: getFormData('
|
|
270
|
+
input: getFormData(' TQ123456'),
|
|
271
|
+
output: { value: getFormData('TQ123456') }
|
|
294
272
|
},
|
|
295
273
|
{
|
|
296
|
-
input: getFormData('
|
|
297
|
-
output: { value: getFormData('
|
|
274
|
+
input: getFormData('TQ123456 '),
|
|
275
|
+
output: { value: getFormData('TQ123456') }
|
|
298
276
|
},
|
|
299
277
|
{
|
|
300
|
-
input: getFormData('
|
|
301
|
-
output: { value: getFormData('
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
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.
|
|
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
|