Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/server/forms/register-as-a-unicorn-breeder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ pages:
path: '/how-many-members-of-staff-will-look-after-the-unicorns'
section: susaYr
next:
- path: '/summary'
- path: '/declaration'
components:
- name: zhJMaM
options:
Expand All @@ -219,7 +219,7 @@ pages:
controller: FileUploadPageController
section: Regnsa
next:
- path: '/declaration'
- path: '/how-many-unicorns-do-you-expect-to-breed-each-year'
components:
- name: dLzALM
title: Documents
Expand Down
14 changes: 0 additions & 14 deletions src/server/plugins/engine/components/EastingNorthingField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,14 +556,7 @@ describe('EastingNorthingField', () => {
easting: 12345.5,
northing: 1234567
}),
// Two errors expected: decimal input triggers both integer validation
// and length validation ('12345.5' is 7 chars, max is 6)
errors: [
expect.objectContaining({
text: expect.stringMatching(
/Easting for .* must be between 1 and 6 digits/
)
}),
expect.objectContaining({
text: expect.stringMatching(
/Easting for .* must be between 1 and 6 digits/
Expand All @@ -582,14 +575,7 @@ describe('EastingNorthingField', () => {
easting: 12345,
northing: 1234567.5
}),
// Two errors expected: decimal input triggers both integer validation
// and length validation ('1234567.5' is 9 chars, max is 7)
errors: [
expect.objectContaining({
text: expect.stringMatching(
/Northing for .* must be between 1 and 7 digits/
)
}),
expect.objectContaining({
text: expect.stringMatching(
/Northing for .* must be between 1 and 7 digits/
Expand Down
28 changes: 4 additions & 24 deletions src/server/plugins/engine/components/EastingNorthingField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,6 @@ const DEFAULT_EASTING_MAX = 700000
const DEFAULT_NORTHING_MIN = 0
const DEFAULT_NORTHING_MAX = 1300000

// Easting length constraints (integer values only, no decimals)
// Min: 1 char for values like "0" or single digit values
// Max: 6 chars for values up to 700000 (British National Grid easting limit)
const EASTING_MIN_LENGTH = 1
const EASTING_MAX_LENGTH = 6

// Northing length constraints (integer values only, no decimals)
// Min: 1 char for values like "0" or single digit values
// Max: 7 chars for values up to 1300000 (British National Grid northing limit)
const NORTHING_MIN_LENGTH = 1
const NORTHING_MAX_LENGTH = 7

export class EastingNorthingField extends FormComponent {
declare options: EastingNorthingFieldComponent['options']
declare formSchema: ObjectSchema<FormPayload>
Expand Down Expand Up @@ -73,9 +61,7 @@ export class EastingNorthingField extends FormComponent {
'number.max': `{{#label}} for ${this.title} must be between ${eastingMin} and {{#limit}}`,
'number.precision': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
'number.integer': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
'number.minLength': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
'number.maxLength': `{{#label}} for ${this.title} must be between 1 and 6 digits`
'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 6 digits`
})

const northingValidationMessages: LanguageMessages =
Expand All @@ -86,9 +72,7 @@ export class EastingNorthingField extends FormComponent {
'number.max': `{{#label}} for ${this.title} must be between ${northingMin} and {{#limit}}`,
'number.precision': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
'number.integer': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
'number.minLength': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
'number.maxLength': `{{#label}} for ${this.title} must be between 1 and 7 digits`
'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 7 digits`
})

this.collection = new ComponentCollection(
Expand All @@ -100,9 +84,7 @@ export class EastingNorthingField extends FormComponent {
schema: {
min: eastingMin,
max: eastingMax,
precision: 0,
minLength: EASTING_MIN_LENGTH,
maxLength: EASTING_MAX_LENGTH
precision: 0
},
options: {
required: isRequired,
Expand All @@ -118,9 +100,7 @@ export class EastingNorthingField extends FormComponent {
schema: {
min: northingMin,
max: northingMax,
precision: 0,
minLength: NORTHING_MIN_LENGTH,
maxLength: NORTHING_MAX_LENGTH
precision: 0
},
options: {
required: isRequired,
Expand Down
28 changes: 4 additions & 24 deletions src/server/plugins/engine/components/LatLongField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ describe('LatLongField', () => {

const result2 = collection.validate(
getFormData({
latitude: '49.1',
latitude: '50.5',
longitude: '-8.9'
})
)
Expand Down Expand Up @@ -578,15 +578,7 @@ describe('LatLongField', () => {
value: getFormData({
latitude: 52,
longitude: -1
}),
errors: [
expect.objectContaining({
text: 'Latitude must have at least 1 decimal place'
}),
expect.objectContaining({
text: 'Longitude must have at least 1 decimal place'
})
]
})
}
},
{
Expand Down Expand Up @@ -619,7 +611,6 @@ describe('LatLongField', () => {
description: 'Length and precision validation',
component: createLatLongComponent(),
assertions: [
// Latitude too short
{
input: getFormData({
latitude: '52',
Expand All @@ -629,12 +620,7 @@ describe('LatLongField', () => {
value: getFormData({
latitude: 52,
longitude: -1.5
}),
errors: [
expect.objectContaining({
text: 'Latitude must have at least 1 decimal place'
})
]
})
}
},
// Latitude too long
Expand All @@ -655,7 +641,6 @@ describe('LatLongField', () => {
]
}
},
// Longitude too short
{
input: getFormData({
latitude: '52.1',
Expand All @@ -665,12 +650,7 @@ describe('LatLongField', () => {
value: getFormData({
latitude: 52.1,
longitude: -1
}),
errors: [
expect.objectContaining({
text: 'Longitude must have at least 1 decimal place'
})
]
})
}
},
// Longitude too long
Expand Down
41 changes: 8 additions & 33 deletions src/server/plugins/engine/components/LatLongField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,6 @@ import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js'
// Precision constants
// UK latitude/longitude requires high precision for accurate location (within ~11mm)
const DECIMAL_PRECISION = 7 // 7 decimal places
const MIN_DECIMAL_PLACES = 1 // At least 1 decimal place required

// Latitude length constraints
// Min: 3 chars for values like "52.1" (2 digits + decimal + 1 decimal place)
// Max: 10 chars for values like "59.1234567" (2 digits + decimal + 7 decimal places)
const LATITUDE_MIN_LENGTH = 3
const LATITUDE_MAX_LENGTH = 10

// Longitude length constraints
// Min: 2 chars for values like "-1" or single digit with decimal (needs min decimal places)
// Max: 10 chars for values like "-1.1234567" (minus + 1 digit + decimal + 7 decimal places)
const LONGITUDE_MIN_LENGTH = 2
const LONGITUDE_MAX_LENGTH = 10

export class LatLongField extends FormComponent {
declare options: LatLongFieldComponent['options']
Expand All @@ -57,38 +44,32 @@ export class LatLongField extends FormComponent {
const isRequired = options.required !== false

// Read schema values from def.schema with fallback defaults
const latitudeMin = schema?.latitude?.min ?? 49
const latitudeMax = schema?.latitude?.max ?? 60
const longitudeMin = schema?.longitude?.min ?? -9
const longitudeMax = schema?.longitude?.max ?? 2
const latitudeMin = schema?.latitude?.min ?? 49.85
const latitudeMax = schema?.latitude?.max ?? 60.859
const longitudeMin = schema?.longitude?.min ?? -13.687
const longitudeMax = schema?.longitude?.max ?? 1.767

const customValidationMessages: LanguageMessages =
convertToLanguageMessages({
'any.required': messageTemplate.objectMissing,
'number.base': messageTemplate.objectMissing,
'number.precision':
'{{#label}} must have no more than 7 decimal places',
'number.minPrecision':
'{{#label}} must have at least {{#minPrecision}} decimal place',
'number.unsafe': '{{#label}} must be a valid number'
})

const latitudeMessages: LanguageMessages = convertToLanguageMessages({
...customValidationMessages,
'number.base': `Enter a valid latitude for ${this.title} like 51.519450`,
'number.min': `Latitude for ${this.title} must be between ${latitudeMin} and ${latitudeMax}`,
'number.max': `Latitude for ${this.title} must be between ${latitudeMin} and ${latitudeMax}`,
'number.minLength': `Latitude for ${this.title} must be between 3 and 10 characters`,
'number.maxLength': `Latitude for ${this.title} must be between 3 and 10 characters`
'number.max': `Latitude for ${this.title} must be between ${latitudeMin} and ${latitudeMax}`
})

const longitudeMessages: LanguageMessages = convertToLanguageMessages({
...customValidationMessages,
'number.base': `Enter a valid longitude for ${this.title} like -0.127758`,
'number.min': `Longitude for ${this.title} must be between ${longitudeMin} and ${longitudeMax}`,
'number.max': `Longitude for ${this.title} must be between ${longitudeMin} and ${longitudeMax}`,
'number.minLength': `Longitude for ${this.title} must be between 2 and 10 characters`,
'number.maxLength': `Longitude for ${this.title} must be between 2 and 10 characters`
'number.max': `Longitude for ${this.title} must be between ${longitudeMin} and ${longitudeMax}`
})

this.collection = new ComponentCollection(
Expand All @@ -100,10 +81,7 @@ export class LatLongField extends FormComponent {
schema: {
min: latitudeMin,
max: latitudeMax,
precision: DECIMAL_PRECISION,
minPrecision: MIN_DECIMAL_PLACES,
minLength: LATITUDE_MIN_LENGTH,
maxLength: LATITUDE_MAX_LENGTH
precision: DECIMAL_PRECISION
},
options: {
required: isRequired,
Expand All @@ -120,10 +98,7 @@ export class LatLongField extends FormComponent {
schema: {
min: longitudeMin,
max: longitudeMax,
precision: DECIMAL_PRECISION,
minPrecision: MIN_DECIMAL_PLACES,
minLength: LONGITUDE_MIN_LENGTH,
maxLength: LONGITUDE_MAX_LENGTH
precision: DECIMAL_PRECISION
},
options: {
required: isRequired,
Expand Down
16 changes: 1 addition & 15 deletions src/server/plugins/engine/components/LocationFieldBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ interface LocationFieldOptions {
interface ValidationConfig {
pattern: RegExp
patternErrorMessage: string
customValidation?: (
value: string,
helpers: joi.CustomHelpers
) => string | joi.ErrorReport
additionalMessages?: LanguageMessages
}

/**
Expand Down Expand Up @@ -71,14 +66,9 @@ export abstract class LocationFieldBase extends FormComponent {
.required()
.pattern(config.pattern)
.messages({
'string.pattern.base': config.patternErrorMessage,
...config.additionalMessages
'string.pattern.base': config.patternErrorMessage
})

if (config.customValidation) {
formSchema = formSchema.custom(config.customValidation)
}

if (locationOptions.required === false) {
formSchema = formSchema.allow('')
}
Expand All @@ -91,10 +81,6 @@ export abstract class LocationFieldBase extends FormComponent {
'string.pattern.base'
]

if (config.additionalMessages) {
messageKeys.push(...Object.keys(config.additionalMessages))
}

const messages = messageKeys.reduce<LanguageMessages>((acc, key) => {
acc[key] = message
return acc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,27 @@ describe('NationalGridFieldNumberField', () => {
})

it('accepts valid values', () => {
const result1 = collection.validate(getFormData('NG12345678'))
const result2 = collection.validate(getFormData('ng12345678'))
const result3 = collection.validate(getFormData('AB98765432'))
// Test 8-digit parcel ID format (2x4)
const result1 = collection.validate(getFormData('TQ12345678'))
const result2 = collection.validate(getFormData('TQ 1234 5678'))

// Test 10-digit OS grid reference format (2x5)
const result3 = collection.validate(getFormData('SU1234567890'))
const result4 = collection.validate(getFormData('SU 12345 67890'))

expect(result1.errors).toBeUndefined()
expect(result2.errors).toBeUndefined()
expect(result3.errors).toBeUndefined()
expect(result4.errors).toBeUndefined()

// Test case-insensitive
const result5 = collection.validate(getFormData('nt12345678'))

expect(result1.errors).toBeUndefined()
expect(result2.errors).toBeUndefined()
expect(result3.errors).toBeUndefined()
expect(result4.errors).toBeUndefined()
expect(result5.errors).toBeUndefined()
})

it('formats values with spaces per GDS guidance', () => {
Expand All @@ -114,8 +128,8 @@ describe('NationalGridFieldNumberField', () => {
const result3 = collection.validate(getFormData('NG12345,678'))

expect(result1.value.myComponent).toBe('NG 1234 5678')
expect(result2.value.myComponent).toBe('NG 1234 5678')
expect(result3.value.myComponent).toBe('NG 1234 5678')
expect(result2.value.myComponent).toBe('NG12345678')
expect(result3.value.myComponent).toBe('NG12345,678')
})

it('adds errors for empty value', () => {
Expand Down Expand Up @@ -258,15 +272,15 @@ describe('NationalGridFieldNumberField', () => {
assertions: [
{
input: getFormData(' NG12345678'),
output: { value: getFormData('NG 1234 5678') }
output: { value: getFormData('NG12345678') }
},
{
input: getFormData('NG12345678 '),
output: { value: getFormData('NG 1234 5678') }
output: { value: getFormData('NG12345678') }
},
{
input: getFormData(' NG12345678 \n\n'),
output: { value: getFormData('NG 1234 5678') }
output: { value: getFormData('NG12345678') }
}
]
},
Expand Down
Loading
Loading