From 3bd3da81e8173b33fbbee5799d8a70e57d93885f Mon Sep 17 00:00:00 2001 From: Michael Harrison Date: Fri, 10 Apr 2026 14:19:55 +0100 Subject: [PATCH 1/2] CCM-15039: update virus scan error messaging --- .../app/preview-letter-template/page.test.tsx | 25 +++++++++----- .../PreviewPdfLetterTemplate.test.tsx | 20 +++++++++-- .../[templateId]/page.tsx | 29 ++++++++++++---- .../PreviewPdfLetterTemplate.tsx | 8 ++++- frontend/src/content/content.ts | 10 +++--- .../template-mgmt-preview-letter-page.ts | 4 +++ ...mgmt-preview-letter-page.component.spec.ts | 33 ++++++++++++++----- 7 files changed, 98 insertions(+), 31 deletions(-) diff --git a/frontend/src/__tests__/app/preview-letter-template/page.test.tsx b/frontend/src/__tests__/app/preview-letter-template/page.test.tsx index 3dc57571c..1d21e58e5 100644 --- a/frontend/src/__tests__/app/preview-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/preview-letter-template/page.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { redirect, RedirectType } from 'next/navigation'; import { getTemplate, getLetterVariantById } from '@utils/form-actions'; @@ -501,22 +501,23 @@ describe('authoring letter template with VALIDATION_FAILED status', () => { ); expect(redirect).not.toHaveBeenCalled(); - expect( - screen.getByRole('alert', { name: 'There is a problem' }) - ).toBeInTheDocument(); + + const errorSummary = screen.getByRole('alert', { + name: 'There is a problem', + }); expect( - screen.getByText('The file(s) you uploaded may contain a virus.') + within(errorSummary).getByText( + 'Your file may contain a virus and we could not open it' + ) ).toBeInTheDocument(); expect( - screen.getByText( - 'Create a new letter template to upload your file(s) again or upload different file(s).' - ) + within(errorSummary).getByText('Upload a different letter template file') ).toBeInTheDocument(); }); - it('does not display submit button when validation has failed', async () => { + it('display "upload different template" button instead of submit button when validation has failed', async () => { jest.mocked(getTemplate).mockResolvedValue({ ...AUTHORING_LETTER_TEMPLATE, templateStatus: 'VALIDATION_FAILED', @@ -532,6 +533,12 @@ describe('authoring letter template with VALIDATION_FAILED status', () => { expect( screen.queryByRole('button', { name: 'Submit template' }) ).not.toBeInTheDocument(); + + expect( + screen.getByRole('button', { + name: 'Upload a different letter template file', + }) + ).toHaveAttribute('href', '/templates/choose-a-template-type'); }); it('does not display error summary when validationErrors is undefined', async () => { diff --git a/frontend/src/__tests__/components/organisms/PreviewPdfLetterTemplate.test.tsx b/frontend/src/__tests__/components/organisms/PreviewPdfLetterTemplate.test.tsx index b7bb4c15c..6fd119199 100644 --- a/frontend/src/__tests__/components/organisms/PreviewPdfLetterTemplate.test.tsx +++ b/frontend/src/__tests__/components/organisms/PreviewPdfLetterTemplate.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, within } from '@testing-library/react'; import { PreviewPdfLetterTemplate } from '@organisms/PreviewPdfLetterTemplate/PreviewPdfLetterTemplate'; import type { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; @@ -100,7 +100,23 @@ describe('PreviewPdfLetterTemplate', () => { render(); - expect(screen.getByRole('alert')).toBeInTheDocument(); + const errorSummary = screen.getByRole('alert'); + + expect( + within(errorSummary).getByText( + 'Your file may contain a virus and we could not open it' + ) + ).toBeInTheDocument(); + + expect( + within(errorSummary).getByText('Upload a different letter template file') + ).toBeInTheDocument(); + + expect( + screen.getByRole('button', { + name: 'Upload a different letter template file', + }) + ).toHaveAttribute('href', '/templates/choose-a-template-type'); }); it('shows error summary for VALIDATION_FAILED status', () => { diff --git a/frontend/src/app/preview-letter-template/[templateId]/page.tsx b/frontend/src/app/preview-letter-template/[templateId]/page.tsx index 2359aad23..1fbb01158 100644 --- a/frontend/src/app/preview-letter-template/[templateId]/page.tsx +++ b/frontend/src/app/preview-letter-template/[templateId]/page.tsx @@ -24,6 +24,7 @@ import { LetterSubmitButton } from '@molecules/LetterRender/LetterSubmitButton'; import { submitAuthoringLetterAction } from './server-action'; import content from '@content/content'; import { NHSNotifyContainer } from '@layouts/container/container'; +import { NHSNotifyButton } from '@atoms/NHSNotifyButton/NHSNotifyButton'; const { approveButtonText, @@ -32,6 +33,7 @@ const { loadingText, pageTitle, validationErrorMessages, + virusScanErrorAction, } = content.pages.previewLetterTemplate; export async function generateMetadata(): Promise { @@ -40,6 +42,15 @@ export async function generateMetadata(): Promise { }; } +function isVirusScanFailed(template: AuthoringLetterTemplate): boolean { + return Boolean( + template.templateStatus === 'VALIDATION_FAILED' && + template.validationErrors?.some( + (error) => error.name === 'VIRUS_SCAN_FAILED' + ) + ); +} + function getValidationErrors(template: AuthoringLetterTemplate): string[] { if (template.templateStatus !== 'VALIDATION_FAILED') return []; @@ -144,12 +155,18 @@ export default async function PreviewLetterTemplatePage({ )}

- - {backLinkText} - + {isVirusScanFailed(validatedTemplate) ? ( + + {virusScanErrorAction} + + ) : ( + + {backLinkText} + + )}

diff --git a/frontend/src/components/organisms/PreviewPdfLetterTemplate/PreviewPdfLetterTemplate.tsx b/frontend/src/components/organisms/PreviewPdfLetterTemplate/PreviewPdfLetterTemplate.tsx index d58a5f643..7d12784da 100644 --- a/frontend/src/components/organisms/PreviewPdfLetterTemplate/PreviewPdfLetterTemplate.tsx +++ b/frontend/src/components/organisms/PreviewPdfLetterTemplate/PreviewPdfLetterTemplate.tsx @@ -169,7 +169,13 @@ export function PreviewPdfLetterTemplate({ )}

- {backLinkText} + {template.templateStatus === 'VIRUS_SCAN_FAILED' ? ( + + {virusScanErrorAction} + + ) : ( + {backLinkText} + )}

diff --git a/frontend/src/content/content.ts b/frontend/src/content/content.ts index e036d90a3..cdb267845 100644 --- a/frontend/src/content/content.ts +++ b/frontend/src/content/content.ts @@ -532,9 +532,8 @@ const previewLetterTemplate = { approveProofText: 'Approve template proof', requestProofText: 'Request a proof', footer: previewLetterFooter, - virusScanError: 'The file(s) you uploaded may contain a virus.', - virusScanErrorAction: - 'Create a new letter template to upload your file(s) again or upload different file(s).', + virusScanError: 'Your file may contain a virus and we could not open it', + virusScanErrorAction: 'Upload a different letter template file', validationError: 'The personalisation fields in your files are missing or do not match.', validationErrorAction: @@ -545,8 +544,8 @@ const previewLetterTemplate = { 'Add the address fields to the template file and upload it.', ], VIRUS_SCAN_FAILED: [ - 'The file(s) you uploaded may contain a virus.', - 'Create a new letter template to upload your file(s) again or upload different file(s).', + 'Your file may contain a virus and we could not open it', + 'Upload a different letter template file', ], // not yet implemented, but required as placeholders INVALID_MARKERS: [], @@ -566,6 +565,7 @@ const previewLetterTemplate = { '{{basePath}}/submit-letter-template/{{templateId}}?lockNumber={{lockNumber}}', requestProofOfTemplate: '{{basePath}}/request-proof-of-template/{{templateId}}?lockNumber={{lockNumber}}', + uploadDifferentTemplateFile: '/templates/choose-a-template-type', }, }; diff --git a/tests/test-team/pages/letter/template-mgmt-preview-letter-page.ts b/tests/test-team/pages/letter/template-mgmt-preview-letter-page.ts index d87acd841..d711be99d 100644 --- a/tests/test-team/pages/letter/template-mgmt-preview-letter-page.ts +++ b/tests/test-team/pages/letter/template-mgmt-preview-letter-page.ts @@ -12,6 +12,7 @@ export class TemplateMgmtPreviewLetterPage extends TemplateMgmtPreviewBasePage { public readonly errorSummary: Locator; public readonly continueButton: Locator; + public readonly uploadDifferentTemplateButton: Locator; public readonly statusTag: Locator; // PDF letter specific @@ -37,6 +38,9 @@ export class TemplateMgmtPreviewLetterPage extends TemplateMgmtPreviewBasePage { this.errorSummary = page.locator('[class="nhsuk-error-summary"]'); this.continueButton = page.locator('[id="preview-letter-template-cta"]'); this.statusTag = page.getByTestId('status-tag'); + this.uploadDifferentTemplateButton = page.getByRole('button', { + name: 'Upload a different letter template file', + }); // PDF letter specific this.pdfLinks = page.locator('[data-testid^="proof-link"]'); diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-preview-letter-page.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-preview-letter-page.component.spec.ts index e2f99b32e..d229246f0 100644 --- a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-preview-letter-page.component.spec.ts +++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-preview-letter-page.component.spec.ts @@ -516,8 +516,6 @@ test.describe('Preview Letter template Page', () => { test('when user visits page with failed virus scan, submit is unavailable and an error is displayed', async ({ page, }) => { - const errorMessage = 'The file(s) you uploaded may contain a virus.'; - const previewLetterTemplatePage = new TemplateMgmtPreviewLetterPage( page ).setPathParam('templateId', templates.virus.id); @@ -531,10 +529,20 @@ test.describe('Preview Letter template Page', () => { ); await expect(previewLetterTemplatePage.errorSummary).toBeVisible(); - await expect(previewLetterTemplatePage.errorSummary).toContainText( - errorMessage - ); + + const errorMessageLines = [ + 'Your file may contain a virus and we could not open it', + 'Upload a different letter template file', + ]; + for (const errorMessage of errorMessageLines) { + await expect(previewLetterTemplatePage.errorSummary).toContainText( + errorMessage + ); + } await expect(previewLetterTemplatePage.continueButton).toBeHidden(); + await expect( + previewLetterTemplatePage.uploadDifferentTemplateButton + ).toHaveAttribute('href', '/templates/choose-a-template-type'); }); test('when user visits page with failed validation, submit is unavailable and an error is displayed', async ({ @@ -1255,13 +1263,22 @@ test.describe('Preview Letter template Page', () => { await expect(previewPage.errorSummary).toBeVisible(); - await expect(previewPage.errorSummary).toContainText( - 'The file(s) you uploaded may contain a virus' - ); + const errorMessageLines = [ + 'Your file may contain a virus and we could not open it', + 'Upload a different letter template file', + ]; + for (const errorMessage of errorMessageLines) { + await expect(previewPage.errorSummary).toContainText(errorMessage); + } await expect(previewPage.continueButton).toBeHidden(); await expect(previewPage.editNameLink).toBeHidden(); + + await expect(previewPage.uploadDifferentTemplateButton).toHaveAttribute( + 'href', + '/templates/choose-a-template-type' + ); }); test('displays missing address lines error when status is VALIDATION_FAILED with MISSING_ADDRESS_LINES', async ({ From f60285506134ea862c4512192310e70ab920bfd7 Mon Sep 17 00:00:00 2001 From: Michael Harrison Date: Fri, 10 Apr 2026 17:45:37 +0100 Subject: [PATCH 2/2] CCM-15039: wip crikey --- .../app/choose-a-template-type/page.test.tsx | 2 +- .../choose-message-order/page.test.tsx | 2 +- .../__snapshots__/page.test.tsx.snap | 1812 ++++++++++++++++- .../app/preview-letter-template/page.test.tsx | 186 +- .../NHSNotifyForm/NHSNotifyForm.test.tsx | 5 +- .../ChooseMessageOrder.test.tsx | 2 +- .../ChooseTemplateType.test.tsx | 2 +- .../forms/CopyTemplate/CopyTemplate.test.tsx | 2 +- .../EmailTemplateForm.test.tsx | 6 +- .../LetterTemplateForm.test.tsx | 7 +- .../NhsAppTemplateForm.test.tsx | 6 +- .../PreviewEmailTemplate.test.tsx | 6 +- .../PreviewNHSAppTemplate.test.tsx | 6 +- .../server-action.test.ts | 6 +- .../PreviewSMSTemplate.test.tsx | 6 +- .../SmsTemplateForm/SmsTemplateForm.test.tsx | 2 +- .../LetterRender/LetterRenderForm.test.tsx | 6 +- .../LetterRender/LetterRenderTab.test.tsx | 6 +- .../LetterRender/server-action.test.ts | 2 +- .../organisms/PreviewDigitalTemplate.test.tsx | 6 +- .../providers/form-provider.test.tsx | 5 +- .../[templateId]/server-action.ts | 2 +- .../[templateId]/server-action.ts | 2 +- .../[templateId]/server-action.ts | 2 +- .../create-message-plan/server-action.ts | 2 +- .../[routingConfigId]/server-action.ts | 2 +- .../[routingConfigId]/actions.ts | 3 +- .../[routingConfigId]/server-action.ts | 2 +- .../[templateId]/page.tsx | 28 +- .../[templateId]/server-action.ts | 2 +- .../[templateId]/server-action.ts | 2 +- .../server-action.ts | 6 +- .../server-action.ts | 6 +- .../server-action.ts | 6 +- .../server-action.ts | 6 +- .../ChooseChannelTemplate.tsx | 2 +- .../ChooseChannelTemplate/server-action.ts | 2 +- .../ChooseLanguageLetterTemplates.tsx | 6 +- .../server-action.ts | 6 +- .../ChooseMessageOrder/ChooseMessageOrder.tsx | 2 +- .../forms/ChooseMessageOrder/server-action.ts | 6 +- .../ChooseTemplateType/ChooseTemplateType.tsx | 6 +- .../forms/ChooseTemplateType/server-action.ts | 2 +- .../forms/CopyTemplate/CopyTemplate.tsx | 6 +- .../forms/CopyTemplate/server-action.ts | 2 +- .../EmailTemplateForm/EmailTemplateForm.tsx | 3 +- .../forms/EmailTemplateForm/server-action.ts | 2 +- .../LetterTemplateForm/LetterTemplateForm.tsx | 3 +- .../forms/LetterTemplateForm/server-action.ts | 6 +- .../NhsAppTemplateForm/NhsAppTemplateForm.tsx | 3 +- .../forms/NhsAppTemplateForm/server-action.ts | 2 +- .../PreviewEmailTemplate.tsx | 7 +- .../PreviewEmailTemplate/server-actions.ts | 6 +- .../PreviewNHSAppTemplate.tsx | 7 +- .../PreviewNHSAppTemplate/server-action.ts | 6 +- .../PreviewSMSTemplate/PreviewSMSTemplate.tsx | 7 +- .../PreviewSMSTemplate/server-actions.ts | 6 +- .../forms/SmsTemplateForm/SmsTemplateForm.tsx | 3 +- .../forms/SmsTemplateForm/server-action.ts | 2 +- .../ChannelTemplates/ChannelTemplates.tsx | 2 +- .../LanguageLetterTemplates.tsx | 2 +- .../LetterRender/LetterRenderTab.tsx | 7 +- .../molecules/LetterRender/server-action.ts | 2 +- .../NHSNotifyRadioButtonForm.tsx | 2 +- .../NhsNotifyErrorSummary.tsx | 5 +- .../PreviewSubmittedTemplate.tsx | 2 +- .../PreviewTemplateFromMessagePlan.tsx | 2 +- .../components/providers/form-provider.tsx | 2 +- frontend/src/content/content.ts | 77 +- frontend/src/utils/client-validate-form.ts | 2 +- frontend/src/utils/escape-markdown.ts | 4 + frontend/src/utils/form-data-to-form-state.ts | 2 +- frontend/src/utils/types.ts | 28 +- utils/utils/src/types.ts | 21 - 74 files changed, 2127 insertions(+), 290 deletions(-) create mode 100644 frontend/src/utils/escape-markdown.ts diff --git a/frontend/src/__tests__/app/choose-a-template-type/page.test.tsx b/frontend/src/__tests__/app/choose-a-template-type/page.test.tsx index 099bf3733..3aaeef4e4 100644 --- a/frontend/src/__tests__/app/choose-a-template-type/page.test.tsx +++ b/frontend/src/__tests__/app/choose-a-template-type/page.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import ChooseATemplateTypePage, { generateMetadata, } from '@app/choose-a-template-type/page'; -import { TemplateFormState } from 'nhs-notify-web-template-management-utils'; +import type { TemplateFormState } from '@utils/types'; import content from '@content/content'; import { useFeatureFlags } from '@providers/client-config-provider'; diff --git a/frontend/src/__tests__/app/message-plans/choose-message-order/page.test.tsx b/frontend/src/__tests__/app/message-plans/choose-message-order/page.test.tsx index 4f46220d5..343b18a31 100644 --- a/frontend/src/__tests__/app/message-plans/choose-message-order/page.test.tsx +++ b/frontend/src/__tests__/app/message-plans/choose-message-order/page.test.tsx @@ -2,7 +2,7 @@ import { render } from '@testing-library/react'; import ChooseMessageOrderPage, { generateMetadata, } from '@app/message-plans/choose-message-order/page'; -import { TemplateFormState } from 'nhs-notify-web-template-management-utils'; +import type { TemplateFormState } from '@utils/types'; import content from '@content/content'; import { useFeatureFlags } from '@providers/client-config-provider'; import { initialFeatureFlags } from '@utils/client-config'; diff --git a/frontend/src/__tests__/app/preview-letter-template/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/preview-letter-template/__snapshots__/page.test.tsx.snap index 9d1f6dbf5..c9b8baed3 100644 --- a/frontend/src/__tests__/app/preview-letter-template/__snapshots__/page.test.tsx.snap +++ b/frontend/src/__tests__/app/preview-letter-template/__snapshots__/page.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`authoring letter template with VALIDATION_FAILED status matches snapshot when validationErrors are present 1`] = ` +exports[`authoring letter template with VALIDATION_FAILED status INVALID_MARKERS matches snapshot 1`] = `
- The template file you uploaded does not contain the address fields. +

+ You used the following personalisation fields with incorrect formatting: +

+

+ {c.compliment} +

+

+ {no.d} +

+

+ {d.underscores_to_test_markdown_escapes} +

+

+ Personalisation fields must start with d. and be inside single curly brackets. For example: {d.fullName} +

+

+ They can only contain +

+
    +
  • + letters (a to z, A to Z) +
  • +
  • + numbers (1 to 9) +
  • +
  • + dashes +
  • +
  • + underscores +
  • +
+

+ Update your letter template file and upload it again +

+ +
+
+
+
+ + Template + +

+ authoring letter template name +

+
+
+
+
+
+ Template ID +
+
+ authoring-letter-template-id +
+
+
+
+
+ Template type +
+
+ Standard letter +
+
+
+
+
+ Total pages +
+
+ 2 +
+
+
+
+
+ Sheets +
+
+ 1 +
+
+ + Learn more + + about sheets + + +
+
+
+
+ Status +
+
+ + Checks failed + +
+
+ + Learn more + + about status + + +
+
+
+
+
+
+ +
+

+ Letter preview +

+

+ Check how your personalisation fields will appear in your letter. +

+ + Learn more about personalising your letters (opens in a new tab). + +
+

+ Example personalisation data +

+ +
+
+
+
+ + +

+ PDS personalisation fields +

+

+ The PDS fields will be pre-filled with example data when you choose a test recipient. +

+
+ + +
+ + + + +
+
+
+