diff --git a/src/client/javascripts/map.js b/src/client/javascripts/map.js index 4cb469d09..98d9fefca 100644 --- a/src/client/javascripts/map.js +++ b/src/client/javascripts/map.js @@ -11,7 +11,6 @@ import createSearchPlugin from '@defra/interactive-map/plugins/search' // @ts-expect-error - no types import maplibreProvider from '@defra/interactive-map/providers/maplibre' import { centroid } from '@turf/centroid' -// @ts-expect-error - no types import OsGridRef, { LatLon } from 'geodesy/osgridref.js' import { processGeospatial } from '~/src/client/javascripts/geospatial-map.js' diff --git a/src/server/plugins/engine/components/ComponentCollection.ts b/src/server/plugins/engine/components/ComponentCollection.ts index c5d04c0de..4074e538c 100644 --- a/src/server/plugins/engine/components/ComponentCollection.ts +++ b/src/server/plugins/engine/components/ComponentCollection.ts @@ -259,10 +259,7 @@ export class ComponentCollection { // Update error with parent title if (translator && parent) { - error.local.title ??= translator.tComponent( - parent as ComponentDef, - 'title' - ) + error.local.title ??= getTranslatedLabel(parent.def, translator) } else { error.local.title ??= parent?.label } diff --git a/src/server/plugins/engine/components/DatePartsField.test.ts b/src/server/plugins/engine/components/DatePartsField.test.ts index 464e9a11a..e22f9ef42 100644 --- a/src/server/plugins/engine/components/DatePartsField.test.ts +++ b/src/server/plugins/engine/components/DatePartsField.test.ts @@ -949,6 +949,90 @@ describe('DatePartsField', () => { }) }) + describe('Validation with translator', () => { + describe.each([ + { + description: 'error description', + def: { + title: 'Example date parts field', + shortDescription: 'Example date parts', + errorDescription: 'Example date error', + name: 'myComponent', + type: ComponentType.DatePartsField, + options: {} + } satisfies DatePartsFieldComponent, + expectedLabel: 'Example date error' + }, + { + description: 'short description', + def: { + title: 'Example date parts field', + shortDescription: 'Example date parts', + name: 'myComponent', + type: ComponentType.DatePartsField, + options: {} + } satisfies DatePartsFieldComponent, + expectedLabel: 'Example date parts' + }, + { + description: 'title', + def: { + title: 'Example date parts field', + name: 'myComponent', + type: ComponentType.DatePartsField, + options: {} + } satisfies DatePartsFieldComponent, + expectedLabel: 'Example date parts field' + } + ])('Error label uses $description', ({ def, expectedLabel }) => { + let collection: ComponentCollection + + beforeEach(() => { + collection = new ComponentCollection([def], { model }) + }) + + it('uses the label for missing date parts', () => { + const result = collection.validate( + getFormData({ + day: '', + month: '', + year: '' + }), + translator + ) + + expect(result.errors).toEqual([ + expect.objectContaining({ + text: `${expectedLabel} must include a day` + }), + expect.objectContaining({ + text: `${expectedLabel} must include a month` + }), + expect.objectContaining({ + text: `${expectedLabel} must include a year` + }) + ]) + }) + + it('uses the label for an invalid date', () => { + const result = collection.validate( + getFormData({ + day: '32', + month: '1', + year: '2024' + }), + translator + ) + + expect(result.errors).toEqual([ + expect.objectContaining({ + text: `${expectedLabel} must be a real date` + }) + ]) + }) + }) + }) + describe('sub-field title key constants', () => { let dateParts: DatePartsField diff --git a/src/server/plugins/engine/components/helpers/geospatial.ts b/src/server/plugins/engine/components/helpers/geospatial.ts index fd4766210..08f7baf3c 100644 --- a/src/server/plugins/engine/components/helpers/geospatial.ts +++ b/src/server/plugins/engine/components/helpers/geospatial.ts @@ -5,7 +5,6 @@ import { import Bourne from '@hapi/bourne' import { booleanWithin } from '@turf/boolean-within' import { point } from '@turf/helpers' -// @ts-expect-error - no types import OsGridRef from 'geodesy/osgridref.js' import JoiBase, { type CustomValidator } from 'joi' diff --git a/src/server/plugins/nunjucks/enviroment.test.js b/src/server/plugins/nunjucks/enviroment.test.js index ccb68eaa2..8de7ccfef 100644 --- a/src/server/plugins/nunjucks/enviroment.test.js +++ b/src/server/plugins/nunjucks/enviroment.test.js @@ -3,14 +3,19 @@ import { environment } from '~/src/server/plugins/nunjucks/environment.js' describe('Nunjucks environment', () => { describe('t global function', () => { + /** @type {typeof tGlobal} */ + let t + + beforeEach(() => { + t = environment.getGlobal('t') + }) + it('has t as a global function', () => { - const globals = /** @type {any} */ (environment).globals - expect(typeof globals.t).toBe('function') + expect(typeof t).toBe('function') }) it('t global returns the English string for a known key', () => { - const globals = /** @type {any} */ (environment).globals - expect(globals.t('errors.title')).toBe('There is a problem') + expect(t('errors.title')).toBe('There is a problem') }) }) @@ -214,6 +219,10 @@ describe('Nunjucks environment', () => { }) }) +/** + * @import { tGlobal } from '~/src/server/plugins/nunjucks/environment.js' + */ + /* * @import { ComponentViewModel } from '~/src/server/plugins/engine/components/types.js' */ diff --git a/src/server/plugins/postcode-lookup/models/index.js b/src/server/plugins/postcode-lookup/models/index.js index ba6067446..4f29a3e98 100644 --- a/src/server/plugins/postcode-lookup/models/index.js +++ b/src/server/plugins/postcode-lookup/models/index.js @@ -298,6 +298,19 @@ export const stepSchema = Joi.string() .valid(...Object.keys(steps)) .required() +/** + * Joi object schema factories typed with their payload, as JavaScript files + * cannot pass type arguments to `Joi.object()` + */ +/** @type {() => ObjectSchema} */ +const detailsObjectSchema = Joi.object.bind(Joi) + +/** @type {() => ObjectSchema} */ +const selectObjectSchema = Joi.object.bind(Joi) + +/** @type {() => ObjectSchema} */ +const manualObjectSchema = Joi.object.bind(Joi) + const sharedPayloadSchemaKeys = { crumb: crumbSchema, step: stepSchema @@ -309,7 +322,7 @@ const sharedPayloadSchemaKeys = { * @returns {ObjectSchema} */ export function createDetailsPayloadSchema(language = 'en-GB') { - return Joi.object() + return detailsObjectSchema() .keys({ ...sharedPayloadSchemaKeys, [postcodeQueryFieldName]: Joi.string() @@ -338,7 +351,7 @@ export function createDetailsPayloadSchema(language = 'en-GB') { * @returns {ObjectSchema} */ export function createSelectPayloadSchema(language = 'en-GB') { - return Joi.object() + return selectObjectSchema() .keys({ ...sharedPayloadSchemaKeys, [uprnFieldName]: Joi.string() @@ -356,7 +369,7 @@ export function createSelectPayloadSchema(language = 'en-GB') { * @returns {ObjectSchema} */ export function createManualPayloadSchema(language = 'en-GB') { - return Joi.object() + return manualObjectSchema() .keys({ ...sharedPayloadSchemaKeys, [line1FieldName]: Joi.string() diff --git a/src/server/plugins/postcode-lookup/routes/index.js b/src/server/plugins/postcode-lookup/routes/index.js index f20eb060d..a3a091e0d 100644 --- a/src/server/plugins/postcode-lookup/routes/index.js +++ b/src/server/plugins/postcode-lookup/routes/index.js @@ -198,6 +198,19 @@ function postRoute(options) { } } +/** + * Validate a payload against a schema. Joi types the value as `any` when + * validation fails, so it is typed here as the partially converted payload. + * @template TSchema + * @param {ObjectSchema} schema + * @param {unknown} payload + * @param {ValidationOptions} [options] + * @returns {{ value: TSchema, error: undefined } | { value: TSchema | undefined, error: ValidationError }} + */ +function validatePayload(schema, payload, options) { + return schema.validate(payload, options) +} + /** * Post handler for the details step * @param {PostcodeLookupPostRequest} request @@ -214,9 +227,10 @@ async function detailsPostHandler(request, h, options) { ) const language = translator.language - const { value: details, error } = createDetailsPayloadSchema( - language - ).validate(request.payload) + const { value: details, error } = validatePayload( + createDetailsPayloadSchema(language), + request.payload + ) let model @@ -252,7 +266,8 @@ async function selectPostHandler(request, h, options) { request.query.language ) const language = translator.language - const { value: select, error } = createSelectPayloadSchema(language).validate( + const { value: select, error } = validatePayload( + createSelectPayloadSchema(language), request.payload ) @@ -295,11 +310,10 @@ async function manualPostHandler(request, h) { ) const language = translator.language - const { value: manual, error } = createManualPayloadSchema(language).validate( + const { value: manual, error } = validatePayload( + createManualPayloadSchema(language), request.payload, - { - abortEarly: false - } + { abortEarly: false } ) if (error) { @@ -317,6 +331,7 @@ async function manualPostHandler(request, h) { /** * @import { ResponseToolkit, ServerRoute } from '@hapi/hapi' + * @import { ObjectSchema, ValidationError, ValidationOptions } from 'joi' * @import { FormMetadata } from '@defra/forms-model' * @import { PostcodeLookupManualPayload, Address, PostcodeLookupGetRequestRefs, PostcodeLookupPostRequestRefs, PostcodeLookupRequest, PostcodeLookupPostRequest, PostcodeLookupConfiguration, PostcodeLookupDispatchData, PostcodeLookupSessionData } from '~/src/server/plugins/postcode-lookup/types.js' * @import { FormRequestPayload, FormResponseToolkit } from '~/src/server/routes/types.js' diff --git a/src/typings/geodesy/index.d.ts b/src/typings/geodesy/index.d.ts new file mode 100644 index 000000000..9d7cfc115 --- /dev/null +++ b/src/typings/geodesy/index.d.ts @@ -0,0 +1,18 @@ +declare module 'geodesy/osgridref.js' { + export class LatLon { + constructor(lat: number, lon: number, height?: number) + get latitude(): number + get longitude(): number + get height(): number + toOsGrid(): OsGridRef + } + + export default class OsGridRef { + constructor(easting: number, northing: number) + easting: number + northing: number + toLatLon(): LatLon + toString(digits?: number): string + static parse(gridref: string): OsGridRef + } +}