import {
ComponentType,
type TextFieldComponent,
type UkAddressFieldComponent
} from '@defra/forms-model'
import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
import { UkAddressField } from '~/src/server/plugins/engine/components/UkAddressField.js'
import {
getAnswer,
type Field
} from '~/src/server/plugins/engine/components/helpers/components.js'
import { type ViewModel } from '~/src/server/plugins/engine/components/types.js'
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
import {
type FormPayload,
type FormState
} from '~/src/server/plugins/engine/types.js'
import definition from '~/test/form/definitions/blank.js'
describe('UkAddressField', () => {
let model: FormModel
beforeEach(() => {
model = new FormModel(definition, {
basePath: 'test'
})
})
describe('Defaults', () => {
let def: UkAddressFieldComponent
let collection: ComponentCollection
let field: Field
beforeEach(() => {
def = {
title: 'Example UK address',
name: 'myComponent',
type: ComponentType.UkAddressField,
options: {}
} satisfies UkAddressFieldComponent
collection = new ComponentCollection([def], { model })
field = collection.fields[0]
})
describe('Schema', () => {
it('uses collection titles as labels', () => {
const { formSchema } = collection
const { keys } = formSchema.describe()
expect(keys).toHaveProperty(
'myComponent__addressLine1',
expect.objectContaining({
flags: expect.objectContaining({ label: 'Address line 1' })
})
)
expect(keys).toHaveProperty(
'myComponent__addressLine2',
expect.objectContaining({
flags: expect.objectContaining({ label: 'Address line 2' })
})
)
expect(keys).toHaveProperty(
'myComponent__town',
expect.objectContaining({
flags: expect.objectContaining({ label: 'Town or city' })
})
)
expect(keys).toHaveProperty(
'myComponent__county',
expect.objectContaining({
flags: expect.objectContaining({ label: 'County' })
})
)
expect(keys).toHaveProperty(
`myComponent__postcode`,
expect.objectContaining({
flags: expect.objectContaining({ label: 'Postcode' })
})
)
})
it('uses collection names as keys', () => {
const { formSchema } = collection
const { keys } = formSchema.describe()
expect(field.keys).toEqual([
'myComponent',
'myComponent__uprn',
'myComponent__addressLine1',
'myComponent__addressLine2',
'myComponent__town',
'myComponent__county',
'myComponent__postcode'
])
expect(field.collection?.keys).not.toHaveProperty('myComponent')
for (const key of field.collection?.keys ?? []) {
expect(keys).toHaveProperty(key)
}
})
it('is required by default', () => {
const { formSchema } = collection
const { keys } = formSchema.describe()
expect(keys).toHaveProperty(
'myComponent__addressLine1',
expect.objectContaining({
flags: expect.objectContaining({ presence: 'required' })
})
)
expect(keys).toHaveProperty(
'myComponent__addressLine2',
expect.objectContaining({
allow: [''], // Required but empty string is allowed
flags: expect.objectContaining({ presence: 'required' })
})
)
expect(keys).toHaveProperty(
'myComponent__town',
expect.objectContaining({
flags: expect.objectContaining({ presence: 'required' })
})
)
expect(keys).toHaveProperty(
'myComponent__county',
expect.objectContaining({
allow: [''], // Required but empty string is allowed
flags: expect.objectContaining({ presence: 'required' })
})
)
expect(keys).toHaveProperty(
`myComponent__postcode`,
expect.objectContaining({
flags: expect.objectContaining({ presence: 'required' })
})
)
})
it('is optional when configured', () => {
const collectionOptional = new ComponentCollection(
[
{
title: 'Example UK address',
name: 'myComponent',
type: ComponentType.UkAddressField,
options: { required: false }
}
],
{ model }
)
const { formSchema } = collectionOptional
const { keys } = formSchema.describe()
expect(keys).toHaveProperty(
'myComponent__addressLine1',
expect.objectContaining({ allow: [''] })
)
expect(keys).toHaveProperty(
'myComponent__addressLine2',
expect.objectContaining({ allow: [''] })
)
expect(keys).toHaveProperty(
'myComponent__town',
expect.objectContaining({ allow: [''] })
)
expect(keys).toHaveProperty(
'myComponent__county',
expect.objectContaining({ allow: [''] })
)
expect(keys).toHaveProperty(
`myComponent__postcode`,
expect.objectContaining({ allow: [''] })
)
const result = collectionOptional.validate(
getFormData({
addressLine1: '',
addressLine2: '',
town: '',
county: '',
postcode: '',
uprn: ''
})
)
expect(result.errors).toBeUndefined()
})
it('accepts valid values', () => {
const result1 = collection.validate(
getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: 'Cheshire',
postcode: 'WA4 1HT',
uprn: ''
})
)
const result2 = collection.validate(
getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: '', // Optional field
town: 'Warrington',
county: '', // Optional field
postcode: 'WA4 1HT',
uprn: ''
})
)
expect(result1.errors).toBeUndefined()
expect(result2.errors).toBeUndefined()
})
it('adds errors for empty value', () => {
const result = collection.validate(
getFormData({
addressLine1: '',
addressLine2: '',
town: '',
county: '',
postcode: '',
uprn: ''
})
)
expect(result.errors).toEqual([
expect.objectContaining({
text: 'Enter address line 1'
}),
expect.objectContaining({
text: 'Enter town or city'
}),
expect.objectContaining({
text: 'Enter postcode'
})
])
})
it('adds errors for invalid values', () => {
const result1 = collection.validate(getFormData({ unknown: 'invalid' }))
const result2 = collection.validate(
getFormData({
addressLine1: ['invalid'],
addressLine2: ['invalid'],
town: ['invalid'],
county: ['invalid'],
postcode: ['invalid']
})
)
const result3 = collection.validate(
getFormData({
addressLine1: 'invalid',
addressLine2: 'invalid',
town: 'invalid',
county: 'invalid',
postcode: 'invalid'
})
)
expect(result1.errors).toBeTruthy()
expect(result2.errors).toBeTruthy()
expect(result3.errors).toBeTruthy()
})
it('should ensure all address fields have valid autocomplete values', () => {
const ukAddressField = new UkAddressField(
{
type: ComponentType.UkAddressField,
name: 'testAddress',
title: 'Test Address',
options: {
required: true,
hideTitle: false
}
},
{
model
}
)
const expectedAutocompleteValues = [
'address-line1',
'address-line2',
'address-level1',
'address-level2',
'postal-code'
]
ukAddressField.collection.components.slice(1).forEach((component) => {
const addressFieldOptions =
component.options as TextFieldComponent['options']
expect(expectedAutocompleteValues).toContain(
addressFieldOptions.autocomplete
)
})
})
})
describe('State', () => {
const address: FormPayload = {
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: 'Cheshire',
postcode: 'WA4 1HT',
uprn: '123456789'
}
it('returns text from state', () => {
const state1 = getFormState(address)
const state2 = getFormState({})
const answer1 = getAnswer(field, state1)
const answer2 = getAnswer(field, state2)
expect(answer1).toBe(
'Richard Fairclough House
Knutsford Road
Warrington
Cheshire
WA4 1HT
'
)
expect(answer2).toBe('')
})
it('returns payload from state', () => {
const state1 = getFormState(address)
const state2 = getFormState({})
const payload1 = field.getFormDataFromState(state1)
const payload2 = field.getFormDataFromState(state2)
expect(payload1).toEqual(getFormData(address))
expect(payload2).toEqual(getFormData({}))
})
it('returns value from state', () => {
const state1 = getFormState(address)
const state2 = getFormState({})
const value1 = field.getFormValueFromState(state1)
const value2 = field.getFormValueFromState(state2)
expect(value1).toEqual(address)
expect(value2).toBeUndefined()
})
it('returns context for conditions and form submission', () => {
const state1 = getFormState(address)
const state2 = getFormState({})
const value1 = field.getContextValueFromState(state1)
const value2 = field.getContextValueFromState(state2)
expect(value1).toEqual([
'Richard Fairclough House',
'Knutsford Road',
'Warrington',
'Cheshire',
'WA4 1HT'
])
expect(value2).toBeNull()
})
it('returns state from payload', () => {
const payload1 = getFormData(address)
const payload2 = getFormData({})
const value1 = field.getStateFromValidForm(payload1)
const value2 = field.getStateFromValidForm(payload2)
expect(value1).toEqual(getFormState(address))
expect(value2).toEqual(getFormState({}))
})
})
describe('View model', () => {
const address: FormPayload = {
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: 'Cheshire',
postcode: 'WA4 1HT'
}
it('sets Nunjucks component defaults', () => {
const payload = getFormData(address)
const viewModel = field.getViewModel(payload)
expect(viewModel).toEqual(
expect.objectContaining({
label: { text: def.title },
name: 'myComponent',
id: 'myComponent',
value: undefined,
components: expect.arrayContaining([
expect.objectContaining({
model: getViewModel(address, 'addressLine1', {
label: { text: 'Address line 1' },
attributes: { autocomplete: 'address-line1' }
})
}),
expect.objectContaining({
model: getViewModel(address, 'addressLine2', {
label: { text: 'Address line 2 (optional)' },
attributes: { autocomplete: 'address-line2' },
value: address.addressLine2
})
}),
expect.objectContaining({
model: getViewModel(address, 'town', {
label: { text: 'Town or city' },
classes: 'govuk-!-width-two-thirds',
attributes: { autocomplete: 'address-level2' },
value: address.town
})
}),
expect.objectContaining({
model: getViewModel(address, 'county', {
label: { text: 'County (optional)' },
attributes: { autocomplete: 'address-level1' },
value: address.county
})
}),
expect.objectContaining({
model: getViewModel(address, 'postcode', {
label: { text: 'Postcode' },
classes: 'govuk-input--width-10',
attributes: { autocomplete: 'postal-code' },
value: address.postcode
})
})
])
})
)
})
it('sets Nunjucks component fieldset', () => {
const payload = getFormData(address)
const viewModel = field.getViewModel(payload)
expect(viewModel.fieldset).toEqual({
legend: {
text: def.title,
classes: 'govuk-fieldset__legend--m'
}
})
})
})
describe('AllPossibleErrors', () => {
it('should return errors', () => {
const errors = field.getAllPossibleErrors()
expect(errors.baseErrors).not.toBeEmpty()
expect(errors.advancedSettingsErrors).toBeEmpty()
})
})
})
describe('Validation', () => {
const address: FormPayload = {
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: 'Cheshire',
postcode: 'WA4 1HT',
uprn: ''
}
const addressLine1Invalid =
'Address line 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
const addressLine2Invalid =
'Address line 2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
const townInvalid =
'Town 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
const countyInvalid =
'County 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
const postcodeInvalid = '111 XX2'
describe.each([
{
description: 'Trim empty spaces',
component: {
title: 'Example UK address',
name: 'myComponent',
type: ComponentType.UkAddressField,
options: {}
} satisfies UkAddressFieldComponent,
assertions: [
{
input: getFormData({
addressLine1: ' Richard Fairclough House',
addressLine2: ' Knutsford Road',
town: ' Warrington',
county: 'Cheshire',
postcode: ' WA4 1HT',
uprn: ''
}),
output: {
value: getFormData(address),
errors: undefined
}
},
{
input: getFormData({
addressLine1: 'Richard Fairclough House ',
addressLine2: 'Knutsford Road ',
town: 'Warrington ',
county: 'Cheshire ',
postcode: 'WA4 1HT ',
uprn: ''
}),
output: {
value: getFormData(address),
errors: undefined
}
},
{
input: getFormData({
addressLine1: ' Richard Fairclough House \n\n',
addressLine2: ' Knutsford Road \n\n',
town: ' Warrington \n\n',
county: ' Cheshire \n\n',
postcode: ' WA4 1HT \n\n',
uprn: ''
}),
output: {
value: getFormData(address),
errors: undefined
}
}
]
},
{
description: 'Out of range values',
component: {
title: 'Example UK address',
name: 'myComponent',
type: ComponentType.UkAddressField,
options: {}
} satisfies UkAddressFieldComponent,
assertions: [
{
input: getFormData({
addressLine1: addressLine1Invalid,
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: 'Cheshire',
postcode: 'WA4 1HT',
uprn: ''
}),
output: {
value: getFormData({
addressLine1: addressLine1Invalid,
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: 'Cheshire',
postcode: 'WA4 1HT',
uprn: ''
}),
errors: [
expect.objectContaining({
text: 'Address line 1 must be 100 characters or less'
})
]
}
},
{
input: getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: addressLine2Invalid,
town: 'Warrington',
county: 'Cheshire',
postcode: 'WA4 1HT',
uprn: ''
}),
output: {
value: getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: addressLine2Invalid,
town: 'Warrington',
county: 'Cheshire',
postcode: 'WA4 1HT',
uprn: ''
}),
errors: [
expect.objectContaining({
text: 'Address line 2 must be 100 characters or less'
})
]
}
},
{
input: getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: townInvalid,
county: 'Cheshire',
postcode: 'WA4 1HT',
uprn: ''
}),
output: {
value: getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: townInvalid,
county: 'Cheshire',
postcode: 'WA4 1HT',
uprn: ''
}),
errors: [
expect.objectContaining({
text: 'Town or city must be 100 characters or less'
})
]
}
},
{
input: getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: countyInvalid,
postcode: 'WA4 1HT',
uprn: ''
}),
output: {
value: getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: countyInvalid,
postcode: 'WA4 1HT',
uprn: ''
}),
errors: [
expect.objectContaining({
text: 'County must be 100 characters or less'
})
]
}
},
{
input: getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: 'Cheshire',
postcode: postcodeInvalid,
uprn: ''
}),
output: {
value: getFormData({
addressLine1: 'Richard Fairclough House',
addressLine2: 'Knutsford Road',
town: 'Warrington',
county: 'Cheshire',
postcode: postcodeInvalid,
uprn: ''
}),
errors: [
expect.objectContaining({
text: 'Enter a valid postcode'
})
]
}
},
{
input: getFormData({
addressLine1: '',
addressLine2: '',
town: '',
county: '',
postcode: postcodeInvalid,
uprn: ''
}),
output: {
value: getFormData({
addressLine1: '',
addressLine2: '',
town: '',
county: '',
postcode: postcodeInvalid,
uprn: ''
}),
errors: [
expect.objectContaining({
text: 'Enter address line 1'
}),
expect.objectContaining({
text: 'Enter town or city'
}),
expect.objectContaining({
text: 'Enter a valid postcode'
})
]
}
}
]
}
])('$description', ({ component: def, assertions }) => {
let collection: ComponentCollection
beforeEach(() => {
collection = new ComponentCollection([def], { model })
})
it.each([...assertions])(
'validates custom example',
({ input, output }) => {
const result = collection.validate(input)
expect(result).toEqual(output)
const errors = collection.getErrors(result.errors)
expect(errors).toEqual(output.errors)
}
)
})
})
})
/**
* UK address field view model
*/
function getViewModel(
address: FormPayload,
name: string,
overrides?: Partial
): Partial {
const payload = getFormData(address)
const fieldName = `myComponent__${name}`
const fieldClasses = overrides?.classes ?? undefined
const fieldAttributes = overrides?.attributes ?? expect.any(Object)
return {
label: expect.objectContaining(
overrides?.label ?? {
text: expect.any(String)
}
),
name: fieldName,
id: fieldName,
value: payload[fieldName],
classes: fieldClasses,
attributes: fieldAttributes
}
}
/**
* UK address form data
*/
function getFormData(address: FormPayload): FormPayload {
return {
myComponent__addressLine1: address.addressLine1,
myComponent__addressLine2: address.addressLine2,
myComponent__town: address.town,
myComponent__county: address.county,
myComponent__postcode: address.postcode,
myComponent__uprn: address.uprn
}
}
/**
* UK address session state
*/
function getFormState(address: FormPayload): FormState {
const [addressLine1, addressLine2, town, county, postcode, uprn] =
Object.values(getFormData(address))
return {
myComponent__addressLine1: addressLine1 ?? null,
myComponent__addressLine2: addressLine2 ?? null,
myComponent__town: town ?? null,
myComponent__county: county ?? null,
myComponent__postcode: postcode ?? null,
myComponent__uprn: uprn ?? null
}
}