@defra/forms-engine-plugin 3.0.3 → 3.0.5
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/common/helpers/redis-client.js +2 -3
- package/.server/server/common/helpers/redis-client.js.map +1 -1
- package/.server/server/plugins/engine/helpers.js +1 -2
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/models/__snapshots__/SummaryViewModel.test.ts.snap +52 -0
- package/.server/server/plugins/engine/options.js +2 -1
- package/.server/server/plugins/engine/options.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +15 -5
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +38 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.js +1 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +2 -2
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
- package/.server/server/plugins/engine/routes/file-upload.js +2 -3
- package/.server/server/plugins/engine/routes/file-upload.js.map +1 -1
- package/.server/server/plugins/engine/services/notifyService.js +1 -2
- package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +1 -1
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/errorPages.js +3 -3
- package/.server/server/plugins/errorPages.js.map +1 -1
- package/package.json +1 -1
- package/src/server/common/helpers/redis-client.js +5 -3
- package/src/server/plugins/engine/helpers.ts +2 -3
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +142 -112
- package/src/server/plugins/engine/models/__snapshots__/SummaryViewModel.test.ts.snap +52 -0
- package/src/server/plugins/engine/options.js +3 -1
- package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +40 -0
- package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +17 -14
- package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +41 -0
- package/src/server/plugins/engine/outputFormatters/machine/v2.ts +1 -1
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +2 -2
- package/src/server/plugins/engine/routes/file-upload.ts +3 -4
- package/src/server/plugins/engine/services/notifyService.test.ts +1 -1
- package/src/server/plugins/engine/services/notifyService.ts +2 -3
- package/src/server/plugins/engine/types.ts +1 -1
- package/src/server/plugins/errorPages.ts +3 -3
|
@@ -45,8 +45,7 @@ export async function submit(context, request, model, emailAddress, items, submi
|
|
|
45
45
|
});
|
|
46
46
|
request.logger.info(logTags, 'Email sent successfully');
|
|
47
47
|
} catch (err) {
|
|
48
|
-
|
|
49
|
-
request.logger.error(errMsg, `[emailSendFailed] Error sending notification email - templateId: ${templateId} - recipient: ${emailAddress} - ${errMsg}`);
|
|
48
|
+
request.logger.error(err, `[emailSendFailed] Error sending notification email - templateId: ${templateId} - recipient: ${emailAddress} - ${getErrorMessage(err)}`);
|
|
50
49
|
throw err;
|
|
51
50
|
}
|
|
52
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifyService.js","names":["getErrorMessage","config","escapeMarkdown","checkFormStatus","getFormatter","sendNotification","templateId","get","submit","context","request","model","emailAddress","items","submitResponse","formMetadata","Promise","resolve","logTags","formStatus","params","logger","info","formName","name","subject","isPreview","outputAudience","def","output","audience","outputVersion","version","outputFormatter","body","Buffer","from","toString","personalisation","err","
|
|
1
|
+
{"version":3,"file":"notifyService.js","names":["getErrorMessage","config","escapeMarkdown","checkFormStatus","getFormatter","sendNotification","templateId","get","submit","context","request","model","emailAddress","items","submitResponse","formMetadata","Promise","resolve","logTags","formStatus","params","logger","info","formName","name","subject","isPreview","outputAudience","def","output","audience","outputVersion","version","outputFormatter","body","Buffer","from","toString","personalisation","err","error"],"sources":["../../../../../src/server/plugins/engine/services/notifyService.ts"],"sourcesContent":["import {\n getErrorMessage,\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { config } from '~/src/config/index.js'\nimport { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers/index.js'\nimport { checkFormStatus } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { getFormatter } from '~/src/server/plugins/engine/outputFormatters/index.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport { type FormRequestPayload } from '~/src/server/routes/types.js'\nimport { sendNotification } from '~/src/server/utils/notify.js'\n\nconst templateId = config.get('notifyTemplateId')\n\n/**\n * Optional GOV.UK Notify service for consumers who want email notifications\n * Can be disabled by not providing notifyTemplateId in config\n * Can be overridden by providing a custom outputService in the services config\n */\nexport async function submit(\n context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload,\n formMetadata?: FormMetadata\n) {\n if (!templateId) {\n return Promise.resolve()\n }\n\n const logTags = ['submit', 'email']\n const formStatus = checkFormStatus(request.params)\n\n // Get submission email personalisation\n request.logger.info(logTags, 'Getting personalisation data')\n\n const formName = escapeMarkdown(model.name)\n const subject = formStatus.isPreview\n ? `TEST FORM SUBMISSION: ${formName}`\n : `Form submission: ${formName}`\n\n const outputAudience = model.def.output?.audience ?? 'human'\n const outputVersion = model.def.output?.version ?? '1'\n\n const outputFormatter = getFormatter(outputAudience, outputVersion)\n let body = outputFormatter(\n context,\n items,\n model,\n submitResponse,\n formStatus,\n formMetadata\n )\n\n // GOV.UK Notify transforms quotes into curly quotes, so we can't just send the raw payload\n // This is logic specific to Notify, so we include the logic here rather than in the formatter\n if (outputAudience === 'machine') {\n body = Buffer.from(body).toString('base64')\n }\n\n request.logger.info(logTags, 'Sending email')\n\n try {\n // Send submission email\n await sendNotification({\n templateId,\n emailAddress,\n personalisation: {\n subject,\n body\n }\n })\n\n request.logger.info(logTags, 'Email sent successfully')\n } catch (err) {\n request.logger.error(\n err,\n `[emailSendFailed] Error sending notification email - templateId: ${templateId} - recipient: ${emailAddress} - ${getErrorMessage(err)}`\n )\n\n throw err\n }\n}\n"],"mappings":"AAAA,SACEA,eAAe,QAGV,oBAAoB;AAE3B,SAASC,MAAM;AACf,SAASC,cAAc;AACvB,SAASC,eAAe;AAGxB,SAASC,YAAY;AAGrB,SAASC,gBAAgB;AAEzB,MAAMC,UAAU,GAAGL,MAAM,CAACM,GAAG,CAAC,kBAAkB,CAAC;;AAEjD;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,MAAMA,CAC1BC,OAAoB,EACpBC,OAA2B,EAC3BC,KAAgB,EAChBC,YAAoB,EACpBC,KAAmB,EACnBC,cAAqC,EACrCC,YAA2B,EAC3B;EACA,IAAI,CAACT,UAAU,EAAE;IACf,OAAOU,OAAO,CAACC,OAAO,CAAC,CAAC;EAC1B;EAEA,MAAMC,OAAO,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC;EACnC,MAAMC,UAAU,GAAGhB,eAAe,CAACO,OAAO,CAACU,MAAM,CAAC;;EAElD;EACAV,OAAO,CAACW,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,8BAA8B,CAAC;EAE5D,MAAMK,QAAQ,GAAGrB,cAAc,CAACS,KAAK,CAACa,IAAI,CAAC;EAC3C,MAAMC,OAAO,GAAGN,UAAU,CAACO,SAAS,GAChC,yBAAyBH,QAAQ,EAAE,GACnC,oBAAoBA,QAAQ,EAAE;EAElC,MAAMI,cAAc,GAAGhB,KAAK,CAACiB,GAAG,CAACC,MAAM,EAAEC,QAAQ,IAAI,OAAO;EAC5D,MAAMC,aAAa,GAAGpB,KAAK,CAACiB,GAAG,CAACC,MAAM,EAAEG,OAAO,IAAI,GAAG;EAEtD,MAAMC,eAAe,GAAG7B,YAAY,CAACuB,cAAc,EAAEI,aAAa,CAAC;EACnE,IAAIG,IAAI,GAAGD,eAAe,CACxBxB,OAAO,EACPI,KAAK,EACLF,KAAK,EACLG,cAAc,EACdK,UAAU,EACVJ,YACF,CAAC;;EAED;EACA;EACA,IAAIY,cAAc,KAAK,SAAS,EAAE;IAChCO,IAAI,GAAGC,MAAM,CAACC,IAAI,CAACF,IAAI,CAAC,CAACG,QAAQ,CAAC,QAAQ,CAAC;EAC7C;EAEA3B,OAAO,CAACW,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,eAAe,CAAC;EAE7C,IAAI;IACF;IACA,MAAMb,gBAAgB,CAAC;MACrBC,UAAU;MACVM,YAAY;MACZ0B,eAAe,EAAE;QACfb,OAAO;QACPS;MACF;IACF,CAAC,CAAC;IAEFxB,OAAO,CAACW,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,yBAAyB,CAAC;EACzD,CAAC,CAAC,OAAOqB,GAAG,EAAE;IACZ7B,OAAO,CAACW,MAAM,CAACmB,KAAK,CAClBD,GAAG,EACH,oEAAoEjC,UAAU,iBAAiBM,YAAY,MAAMZ,eAAe,CAACuC,GAAG,CAAC,EACvI,CAAC;IAED,MAAMA,GAAG;EACX;AACF","ignoreList":[]}
|
|
@@ -317,7 +317,7 @@ export type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {
|
|
|
317
317
|
};
|
|
318
318
|
export type RichFormValue = FormValue | FormPayload | DatePartsState | MonthYearState | UkAddressState;
|
|
319
319
|
export interface FormAdapterSubmissionMessageData {
|
|
320
|
-
main: Record<string, RichFormValue>;
|
|
320
|
+
main: Record<string, RichFormValue | null>;
|
|
321
321
|
repeaters: Record<string, Record<string, RichFormValue>[]>;
|
|
322
322
|
files: Record<string, FormAdapterFile[]>;
|
|
323
323
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage\n extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAqDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AA8Nd;AACA;AACA","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue | null>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage\n extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAqDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AA8Nd;AACA;AACA","ignoreList":[]}
|
|
@@ -11,13 +11,13 @@ export default {
|
|
|
11
11
|
// An error was raised during
|
|
12
12
|
// processing the request
|
|
13
13
|
const statusCode = response.output.statusCode;
|
|
14
|
-
const
|
|
14
|
+
const err = {
|
|
15
15
|
statusCode,
|
|
16
16
|
message: response.message,
|
|
17
17
|
stack: response.stack
|
|
18
18
|
};
|
|
19
|
-
request.logger.error(
|
|
20
|
-
return h.response(
|
|
19
|
+
request.logger.error(err, `[httpError] HTTP ${statusCode} error occurred - ${response.message} - path: ${request.path} - method: ${request.method}`);
|
|
20
|
+
return h.response(err).code(statusCode);
|
|
21
21
|
}
|
|
22
22
|
return h.continue;
|
|
23
23
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errorPages.js","names":["plugin","name","register","server","ext","request","h","response","isBoom","statusCode","output","
|
|
1
|
+
{"version":3,"file":"errorPages.js","names":["plugin","name","register","server","ext","request","h","response","isBoom","statusCode","output","err","message","stack","logger","error","path","method","code","continue"],"sources":["../../../src/server/plugins/errorPages.ts"],"sourcesContent":["import {\n type Request,\n type ResponseToolkit,\n type ServerRegisterPluginObject\n} from '@hapi/hapi'\n\n/*\n * Add an `onPreResponse` listener to return error pages\n */\nexport default {\n plugin: {\n name: 'error-pages',\n register(server) {\n server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {\n const response = request.response\n\n if ('isBoom' in response && response.isBoom) {\n // An error was raised during\n // processing the request\n const statusCode = response.output.statusCode\n\n const err = {\n statusCode,\n message: response.message,\n stack: response.stack\n }\n\n request.logger.error(\n err,\n `[httpError] HTTP ${statusCode} error occurred - ${response.message} - path: ${request.path} - method: ${request.method}`\n )\n\n return h.response(err).code(statusCode)\n }\n\n return h.continue\n })\n }\n }\n} satisfies ServerRegisterPluginObject<void>\n"],"mappings":"AAMA;AACA;AACA;AACA,eAAe;EACbA,MAAM,EAAE;IACNC,IAAI,EAAE,aAAa;IACnBC,QAAQA,CAACC,MAAM,EAAE;MACfA,MAAM,CAACC,GAAG,CAAC,eAAe,EAAE,CAACC,OAAgB,EAAEC,CAAkB,KAAK;QACpE,MAAMC,QAAQ,GAAGF,OAAO,CAACE,QAAQ;QAEjC,IAAI,QAAQ,IAAIA,QAAQ,IAAIA,QAAQ,CAACC,MAAM,EAAE;UAC3C;UACA;UACA,MAAMC,UAAU,GAAGF,QAAQ,CAACG,MAAM,CAACD,UAAU;UAE7C,MAAME,GAAG,GAAG;YACVF,UAAU;YACVG,OAAO,EAAEL,QAAQ,CAACK,OAAO;YACzBC,KAAK,EAAEN,QAAQ,CAACM;UAClB,CAAC;UAEDR,OAAO,CAACS,MAAM,CAACC,KAAK,CAClBJ,GAAG,EACH,oBAAoBF,UAAU,qBAAqBF,QAAQ,CAACK,OAAO,YAAYP,OAAO,CAACW,IAAI,cAAcX,OAAO,CAACY,MAAM,EACzH,CAAC;UAED,OAAOX,CAAC,CAACC,QAAQ,CAACI,GAAG,CAAC,CAACO,IAAI,CAACT,UAAU,CAAC;QACzC;QAEA,OAAOH,CAAC,CAACa,QAAQ;MACnB,CAAC,CAAC;IACJ;EACF;AACF,CAAC","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -63,9 +63,11 @@ export function buildRedisClient() {
|
|
|
63
63
|
)
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
redisClient.on('error', (
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
redisClient.on('error', (err) => {
|
|
67
|
+
logger.error(
|
|
68
|
+
err,
|
|
69
|
+
`[redisConnectionError] Redis connection error - ${getErrorMessage(err)}`
|
|
70
|
+
)
|
|
69
71
|
})
|
|
70
72
|
|
|
71
73
|
return redisClient
|
|
@@ -149,10 +149,9 @@ export function encodeUrl(link?: string) {
|
|
|
149
149
|
try {
|
|
150
150
|
return new URL(link).toString() // escape the search params without breaking the ? and & reserved characters in rfc2368
|
|
151
151
|
} catch (err) {
|
|
152
|
-
const errMsg = getErrorMessage(err)
|
|
153
152
|
logger.error(
|
|
154
|
-
|
|
155
|
-
`[urlEncodingFailed] Failed to encode URL: ${link} - ${
|
|
153
|
+
err,
|
|
154
|
+
`[urlEncodingFailed] Failed to encode URL: ${link} - ${getErrorMessage(err)}`
|
|
156
155
|
)
|
|
157
156
|
throw err
|
|
158
157
|
}
|
|
@@ -65,7 +65,9 @@ describe('SummaryViewModel', () => {
|
|
|
65
65
|
'Pizzas',
|
|
66
66
|
'Pizza'
|
|
67
67
|
],
|
|
68
|
-
values: ['Collection', 'Not supplied']
|
|
68
|
+
values: ['Collection', 'Not supplied'],
|
|
69
|
+
answers: ['Collection', ''],
|
|
70
|
+
names: ['orderType', 'pizza']
|
|
69
71
|
},
|
|
70
72
|
{
|
|
71
73
|
description: '1 item',
|
|
@@ -87,7 +89,9 @@ describe('SummaryViewModel', () => {
|
|
|
87
89
|
'Pizzas',
|
|
88
90
|
'Pizza'
|
|
89
91
|
],
|
|
90
|
-
values: ['Delivery', 'You added 1 Pizza']
|
|
92
|
+
values: ['Delivery', 'You added 1 Pizza'],
|
|
93
|
+
answers: ['Delivery', 'You added 1 Pizza'],
|
|
94
|
+
names: ['orderType', 'pizza']
|
|
91
95
|
},
|
|
92
96
|
{
|
|
93
97
|
description: '2 items',
|
|
@@ -114,142 +118,168 @@ describe('SummaryViewModel', () => {
|
|
|
114
118
|
'Pizzas',
|
|
115
119
|
'Pizza'
|
|
116
120
|
],
|
|
117
|
-
values: ['Delivery', 'You added 2 Pizzas']
|
|
121
|
+
values: ['Delivery', 'You added 2 Pizzas'],
|
|
122
|
+
answers: ['Delivery', 'You added 2 Pizzas'],
|
|
123
|
+
names: ['orderType', 'pizza']
|
|
118
124
|
}
|
|
119
|
-
])(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
])(
|
|
126
|
+
'Check answers ($description)',
|
|
127
|
+
({ state, keys, values, names, answers }) => {
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
context = model.getFormContext(request, state)
|
|
130
|
+
summaryViewModel = new SummaryViewModel(request, page, context)
|
|
131
|
+
})
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
it('should add title for each section', () => {
|
|
134
|
+
const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
|
|
127
135
|
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
// 1st summary list has no title
|
|
137
|
+
expect(checkAnswers1).toHaveProperty('title', undefined)
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
// 2nd summary list has section title
|
|
140
|
+
expect(checkAnswers2).toHaveProperty('title', {
|
|
141
|
+
text: 'Food'
|
|
142
|
+
})
|
|
134
143
|
})
|
|
135
|
-
})
|
|
136
144
|
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
it('should add summary list for each section', () => {
|
|
146
|
+
expect(summaryViewModel.checkAnswers).toHaveLength(2)
|
|
139
147
|
|
|
140
|
-
|
|
148
|
+
const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
const { summaryList: summaryList1 } = checkAnswers1
|
|
151
|
+
const { summaryList: summaryList2 } = checkAnswers2
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
expect(summaryList1).toHaveProperty('rows', [
|
|
154
|
+
{
|
|
155
|
+
key: {
|
|
156
|
+
text: keys[2]
|
|
157
|
+
},
|
|
158
|
+
value: {
|
|
159
|
+
classes: 'app-prose-scope',
|
|
160
|
+
html: values[0]
|
|
161
|
+
},
|
|
162
|
+
actions: {
|
|
163
|
+
items: [
|
|
164
|
+
{
|
|
165
|
+
classes: 'govuk-link--no-visited-state',
|
|
166
|
+
href: `${basePath}/delivery-or-collection?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
|
|
167
|
+
text: 'Change',
|
|
168
|
+
visuallyHiddenText: keys[0]
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
163
172
|
}
|
|
164
|
-
|
|
165
|
-
])
|
|
173
|
+
])
|
|
166
174
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
expect(summaryList2).toHaveProperty('rows', [
|
|
176
|
+
{
|
|
177
|
+
key: {
|
|
178
|
+
text: keys[1]
|
|
179
|
+
},
|
|
180
|
+
value: {
|
|
181
|
+
classes: 'app-prose-scope',
|
|
182
|
+
html: values[1]
|
|
183
|
+
},
|
|
184
|
+
actions: {
|
|
185
|
+
items: [
|
|
186
|
+
{
|
|
187
|
+
classes: 'govuk-link--no-visited-state',
|
|
188
|
+
href: `${basePath}/pizza-order/summary?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
|
|
189
|
+
text: 'Change',
|
|
190
|
+
visuallyHiddenText: 'Pizza'
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
185
194
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
})
|
|
195
|
+
])
|
|
196
|
+
})
|
|
189
197
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
it('should add summary list for each section (preview URL direct access)', () => {
|
|
199
|
+
request.query.force = '' // Preview URL '?force'
|
|
200
|
+
context = model.getFormContext(request, state)
|
|
201
|
+
summaryViewModel = new SummaryViewModel(request, page, context)
|
|
194
202
|
|
|
195
|
-
|
|
203
|
+
expect(summaryViewModel.checkAnswers).toHaveLength(2)
|
|
196
204
|
|
|
197
|
-
|
|
205
|
+
const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
|
|
198
206
|
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
const { summaryList: summaryList1 } = checkAnswers1
|
|
208
|
+
const { summaryList: summaryList2 } = checkAnswers2
|
|
201
209
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
expect(summaryList1).toHaveProperty('rows', [
|
|
211
|
+
{
|
|
212
|
+
key: {
|
|
213
|
+
text: keys[2]
|
|
214
|
+
},
|
|
215
|
+
value: {
|
|
216
|
+
classes: 'app-prose-scope',
|
|
217
|
+
html: values[0]
|
|
218
|
+
},
|
|
219
|
+
actions: {
|
|
220
|
+
items: []
|
|
221
|
+
}
|
|
213
222
|
}
|
|
214
|
-
|
|
215
|
-
])
|
|
223
|
+
])
|
|
216
224
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
expect(summaryList2).toHaveProperty('rows', [
|
|
226
|
+
{
|
|
227
|
+
key: {
|
|
228
|
+
text: keys[1]
|
|
229
|
+
},
|
|
230
|
+
value: {
|
|
231
|
+
classes: 'app-prose-scope',
|
|
232
|
+
html: values[1]
|
|
233
|
+
},
|
|
234
|
+
actions: {
|
|
235
|
+
items: []
|
|
236
|
+
}
|
|
228
237
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
})
|
|
238
|
+
])
|
|
239
|
+
})
|
|
232
240
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
it('should use correct summary labels', () => {
|
|
242
|
+
request.query.force = '' // Preview URL '?force'
|
|
243
|
+
context = model.getFormContext(request, state)
|
|
244
|
+
summaryViewModel = new SummaryViewModel(request, page, context)
|
|
237
245
|
|
|
238
|
-
|
|
246
|
+
expect(summaryViewModel.details).toHaveLength(2)
|
|
239
247
|
|
|
240
|
-
|
|
248
|
+
const [details1, details2] = summaryViewModel.details
|
|
241
249
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
expect(details1.items[0]).toMatchObject({
|
|
251
|
+
name: names[0],
|
|
252
|
+
value: answers[0],
|
|
253
|
+
title: keys[2],
|
|
254
|
+
label: keys[0]
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
expect(details2.items[0]).toMatchObject({
|
|
258
|
+
name: names[1],
|
|
259
|
+
value: answers[1],
|
|
260
|
+
title: keys[1],
|
|
261
|
+
label: keys[4]
|
|
262
|
+
})
|
|
246
263
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
264
|
+
const snapshot = [
|
|
265
|
+
{
|
|
266
|
+
name: names[0],
|
|
267
|
+
value: answers[0],
|
|
268
|
+
title: keys[2],
|
|
269
|
+
label: keys[0]
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: names[1],
|
|
273
|
+
value: answers[1],
|
|
274
|
+
title: keys[1],
|
|
275
|
+
label: keys[4]
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
expect(snapshot).toMatchSnapshot()
|
|
250
280
|
})
|
|
251
|
-
}
|
|
252
|
-
|
|
281
|
+
}
|
|
282
|
+
)
|
|
253
283
|
})
|
|
254
284
|
|
|
255
285
|
describe('SummaryPageController', () => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
|
2
|
+
|
|
3
|
+
exports[`SummaryViewModel Check answers (0 items) should use correct summary labels 1`] = `
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"label": "How would you like to receive your pizza?",
|
|
7
|
+
"name": "orderType",
|
|
8
|
+
"title": "How you would like to receive your pizza",
|
|
9
|
+
"value": "Collection",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"label": "Pizza",
|
|
13
|
+
"name": "pizza",
|
|
14
|
+
"title": "Pizzas",
|
|
15
|
+
"value": "",
|
|
16
|
+
},
|
|
17
|
+
]
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
exports[`SummaryViewModel Check answers (1 item) should use correct summary labels 1`] = `
|
|
21
|
+
[
|
|
22
|
+
{
|
|
23
|
+
"label": "How would you like to receive your pizza?",
|
|
24
|
+
"name": "orderType",
|
|
25
|
+
"title": "How you would like to receive your pizza",
|
|
26
|
+
"value": "Delivery",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"label": "Pizza",
|
|
30
|
+
"name": "pizza",
|
|
31
|
+
"title": "Pizza added",
|
|
32
|
+
"value": "You added 1 Pizza",
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
exports[`SummaryViewModel Check answers (2 items) should use correct summary labels 1`] = `
|
|
38
|
+
[
|
|
39
|
+
{
|
|
40
|
+
"label": "How would you like to receive your pizza?",
|
|
41
|
+
"name": "orderType",
|
|
42
|
+
"title": "How you would like to receive your pizza",
|
|
43
|
+
"value": "Delivery",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"label": "Pizza",
|
|
47
|
+
"name": "pizza",
|
|
48
|
+
"title": "Pizzas added",
|
|
49
|
+
"value": "You added 2 Pizzas",
|
|
50
|
+
},
|
|
51
|
+
]
|
|
52
|
+
`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getErrorMessage } from '@defra/forms-model'
|
|
1
2
|
import Joi from 'joi'
|
|
2
3
|
|
|
3
4
|
import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
|
|
@@ -39,7 +40,8 @@ export function validatePluginOptions(options) {
|
|
|
39
40
|
|
|
40
41
|
if (result.error) {
|
|
41
42
|
logger.error(
|
|
42
|
-
|
|
43
|
+
result.error,
|
|
44
|
+
`Missing required properties in plugin options: ${getErrorMessage(result.error)}`
|
|
43
45
|
)
|
|
44
46
|
throw new Error('Invalid plugin options', result.error)
|
|
45
47
|
}
|
|
@@ -723,6 +723,46 @@ describe('Adapter v1 formatter', () => {
|
|
|
723
723
|
expect(parsedBody.meta.versionMetadata).toBeUndefined()
|
|
724
724
|
})
|
|
725
725
|
|
|
726
|
+
it('should handle optional fields that are undefined', () => {
|
|
727
|
+
const formMetadata: Partial<FormMetadata> = {
|
|
728
|
+
id: 'form-123',
|
|
729
|
+
slug: 'test-form',
|
|
730
|
+
title: 'Test Form',
|
|
731
|
+
notificationEmail: 'test@example.com'
|
|
732
|
+
} as FormMetadata
|
|
733
|
+
|
|
734
|
+
const formStatus = {
|
|
735
|
+
isPreview: false,
|
|
736
|
+
state: FormStatus.Live
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const dummyField: Field = {
|
|
740
|
+
getFormValueFromState: (_) => undefined
|
|
741
|
+
} as Field
|
|
742
|
+
|
|
743
|
+
const items: DetailItem[] = [
|
|
744
|
+
{
|
|
745
|
+
name: 'exampleField3',
|
|
746
|
+
label: 'Example Field 3',
|
|
747
|
+
href: '/example-field-3',
|
|
748
|
+
title: 'Example Field 3 Title',
|
|
749
|
+
field: dummyField,
|
|
750
|
+
value: ''
|
|
751
|
+
} as DetailItemField
|
|
752
|
+
]
|
|
753
|
+
|
|
754
|
+
const body = format(
|
|
755
|
+
context,
|
|
756
|
+
items,
|
|
757
|
+
model,
|
|
758
|
+
submitResponse,
|
|
759
|
+
formStatus,
|
|
760
|
+
formMetadata as FormMetadata
|
|
761
|
+
)
|
|
762
|
+
const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
|
|
763
|
+
expect(parsedBody.data.main).toEqual({ exampleField3: null })
|
|
764
|
+
})
|
|
765
|
+
|
|
726
766
|
describe('version metadata handling', () => {
|
|
727
767
|
it('should include versionMetadata when context has submittedVersionNumber and formMetadata has versions', () => {
|
|
728
768
|
const formMetadata: Partial<FormMetadata> = {
|