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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
},
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@defra/forms-model": "^3.0.574",
"@defra/forms-model": "^3.0.575",
"@defra/hapi-tracing": "^1.26.0",
"@elastic/ecs-pino-format": "^1.5.0",
"@hapi/boom": "^10.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,10 +556,17 @@ 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 5 digits/
/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 @@ -575,7 +582,14 @@ 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
48 changes: 38 additions & 10 deletions src/server/plugins/engine/components/EastingNorthingField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,22 @@ import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js'

// British National Grid coordinate limits
const DEFAULT_EASTING_MIN = 0
const DEFAULT_EASTING_MAX = 70000
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 @@ -59,9 +71,11 @@ export class EastingNorthingField extends FormComponent {
'number.base': messageTemplate.objectMissing,
'number.min': `{{#label}} for ${this.title} must be between {{#limit}} and ${eastingMax}`,
'number.max': `{{#label}} for ${this.title} must be between ${eastingMin} and {{#limit}}`,
'number.precision': `{{#label}} for ${this.title} must be between 1 and 5 digits`,
'number.integer': `{{#label}} for ${this.title} must be between 1 and 5 digits`,
'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 5 digits`
'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`
})

const northingValidationMessages: LanguageMessages =
Expand All @@ -72,7 +86,9 @@ 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.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`
})

this.collection = new ComponentCollection(
Expand All @@ -81,7 +97,13 @@ export class EastingNorthingField extends FormComponent {
type: ComponentType.NumberField,
name: `${name}__easting`,
title: 'Easting',
schema: { min: eastingMin, max: eastingMax, precision: 0 },
schema: {
min: eastingMin,
max: eastingMax,
precision: 0,
minLength: EASTING_MIN_LENGTH,
maxLength: EASTING_MAX_LENGTH
},
options: {
required: isRequired,
optionalText: true,
Expand All @@ -93,7 +115,13 @@ export class EastingNorthingField extends FormComponent {
type: ComponentType.NumberField,
name: `${name}__northing`,
title: 'Northing',
schema: { min: northingMin, max: northingMax, precision: 0 },
schema: {
min: northingMin,
max: northingMax,
precision: 0,
minLength: NORTHING_MIN_LENGTH,
maxLength: NORTHING_MAX_LENGTH
},
options: {
required: isRequired,
optionalText: true,
Expand Down Expand Up @@ -179,7 +207,7 @@ export class EastingNorthingField extends FormComponent {
{
type: 'eastingFormat',
template:
'Easting for [short description] must be between 1 and 5 digits'
'Easting for [short description] must be between 1 and 6 digits'
},
{
type: 'northingFormat',
Expand All @@ -190,11 +218,11 @@ export class EastingNorthingField extends FormComponent {
advancedSettingsErrors: [
{
type: 'eastingMin',
template: `Easting for [short description] must be between ${DEFAULT_EASTING_MIN} and ${DEFAULT_EASTING_MAX}`
template: `Easting for [short description] must be between 0 and 700000`
},
{
type: 'eastingMax',
template: `Easting for [short description] must be between ${DEFAULT_EASTING_MIN} and ${DEFAULT_EASTING_MAX}`
template: `Easting for [short description] must be between 0 and 700000`
},
{
type: 'northingMin',
Expand Down
189 changes: 173 additions & 16 deletions src/server/plugins/engine/components/LatLongField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ describe('LatLongField', () => {

const result2 = collection.validate(
getFormData({
latitude: '49',
longitude: '-9'
latitude: '49.1',
longitude: '-8.9'
})
)

Expand Down Expand Up @@ -381,13 +381,7 @@ describe('LatLongField', () => {
describe.each([
{
description: 'Trim empty spaces',
component: {
title: 'Example lat long',
name: 'myComponent',
type: ComponentType.LatLongField,
options: {},
schema: {}
} satisfies LatLongFieldComponent,
component: createLatLongComponent(),
assertions: [
{
input: getFormData({
Expand Down Expand Up @@ -571,15 +565,162 @@ describe('LatLongField', () => {
}
]
},
{
description: 'Minimum precision validation',
component: createLatLongComponent(),
assertions: [
{
input: getFormData({
latitude: '52',
longitude: '-1'
}),
output: {
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'
})
]
}
},
{
input: getFormData({
latitude: '52.1',
longitude: '-1.5'
}),
output: {
value: getFormData({
latitude: 52.1,
longitude: -1.5
})
}
},
{
input: getFormData({
latitude: '52.123456',
longitude: '-1.123456'
}),
output: {
value: getFormData({
latitude: 52.123456,
longitude: -1.123456
})
}
}
]
},
{
description: 'Length and precision validation',
component: createLatLongComponent(),
assertions: [
// Latitude too short
{
input: getFormData({
latitude: '52',
longitude: '-1.5'
}),
output: {
value: getFormData({
latitude: 52,
longitude: -1.5
}),
errors: [
expect.objectContaining({
text: 'Latitude must have at least 1 decimal place'
})
]
}
},
// Latitude too long
{
input: getFormData({
latitude: '52.12345678',
longitude: '-1.5'
}),
output: {
value: getFormData({
latitude: 52.12345678,
longitude: -1.5
}),
errors: [
expect.objectContaining({
text: 'Latitude must have no more than 7 decimal places'
})
]
}
},
// Longitude too short
{
input: getFormData({
latitude: '52.1',
longitude: '-1'
}),
output: {
value: getFormData({
latitude: 52.1,
longitude: -1
}),
errors: [
expect.objectContaining({
text: 'Longitude must have at least 1 decimal place'
})
]
}
},
// Longitude too long
{
input: getFormData({
latitude: '52.1',
longitude: '-1.12345678'
}),
output: {
value: getFormData({
latitude: 52.1,
longitude: -1.12345678
}),
errors: [
expect.objectContaining({
text: 'Longitude must have no more than 7 decimal places'
})
]
}
},
// Valid values
{
input: getFormData({
latitude: '52.1',
longitude: '-1.5'
}),
output: {
value: getFormData({
latitude: 52.1,
longitude: -1.5
})
}
},
{
input: getFormData({
latitude: '52.1234',
longitude: '-1.123'
}),
output: {
value: getFormData({
latitude: 52.1234,
longitude: -1.123
})
}
}
]
},
{
description: 'Invalid format',
component: {
title: 'Example lat long',
name: 'myComponent',
type: ComponentType.LatLongField,
options: {},
schema: {}
} satisfies LatLongFieldComponent,
component: createLatLongComponent(),
assertions: [
{
input: getFormData({
Expand Down Expand Up @@ -665,6 +806,22 @@ describe('LatLongField', () => {
})
})

/**
* Factory function to create a default LatLongField component with optional overrides
*/
function createLatLongComponent(
overrides: Partial<LatLongFieldComponent> = {}
): LatLongFieldComponent {
return {
title: 'Example lat long',
name: 'myComponent',
type: ComponentType.LatLongField,
options: {},
schema: {},
...overrides
} satisfies LatLongFieldComponent
}

function getFormData(
value:
| { latitude?: string | number; longitude?: string | number }
Expand Down
Loading
Loading