diff --git a/frontend/src/__tests__/helpers/helpers.ts b/frontend/src/__tests__/helpers/helpers.ts index 5a5afe5de..c21054001 100644 --- a/frontend/src/__tests__/helpers/helpers.ts +++ b/frontend/src/__tests__/helpers/helpers.ts @@ -1,5 +1,6 @@ import { mockDeep } from 'jest-mock-extended'; import { + ContactDetailDto, LetterVariant, ProofRequest, RoutingConfig, @@ -225,3 +226,25 @@ export const makeSmsProofRequest = ( contactDetailValue: '07123456789', ...overrides, }); + +export const makeEmailContactDetail = ( + overrides: Partial = {} +): ContactDetailDto => ({ + id: 'contact-123', + rawValue: ' TEST@example.com ', + value: 'test@example.com', + type: 'EMAIL', + status: 'VERIFIED', + ...overrides, +}); + +export const makeSmsContactDetail = ( + overrides: Partial = {} +): ContactDetailDto => ({ + id: 'contact-456', + rawValue: ' 0712 345 6789 ', + value: '+447123456789', + type: 'SMS', + status: 'VERIFIED', + ...overrides, +}); diff --git a/frontend/src/app/email-address-confirmed/[contactDetailId]/__tests__/__snapshots__/page.test.tsx.snap b/frontend/src/app/email-address-confirmed/[contactDetailId]/__tests__/__snapshots__/page.test.tsx.snap new file mode 100644 index 000000000..1879211c9 --- /dev/null +++ b/frontend/src/app/email-address-confirmed/[contactDetailId]/__tests__/__snapshots__/page.test.tsx.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders page correctly should match snapshot with templateId 1`] = ` + +
+
+
+
+
+

+ Email address confirmed +

+
+

+ You can now send test emails to + + TEST@example.com + +

+

+ This email address will only be available for you to use. If someone else needs to send test emails to it, they must add it to their own account. +

+
+
+ + Continue to send a test email + +
+
+
+
+
+`; + +exports[`renders page correctly should match snapshot without templateId 1`] = ` + +
+
+
+
+
+

+ Email address confirmed +

+
+

+ You can now send test emails to + + TEST@example.com + +

+

+ This email address will only be available for you to use. If someone else needs to send test emails to it, they must add it to their own account. +

+
+
+ + Back to all templates + +
+
+
+
+
+`; diff --git a/frontend/src/app/email-address-confirmed/[contactDetailId]/__tests__/page.test.tsx b/frontend/src/app/email-address-confirmed/[contactDetailId]/__tests__/page.test.tsx new file mode 100644 index 000000000..7591524e8 --- /dev/null +++ b/frontend/src/app/email-address-confirmed/[contactDetailId]/__tests__/page.test.tsx @@ -0,0 +1,165 @@ +import { render } from '@testing-library/react'; +import { redirect } from 'next/navigation'; +import { serverIsFeatureEnabled } from '@utils/server-features'; +import Page, { + generateMetadata, +} from '@app/email-address-confirmed/[contactDetailId]/page'; +import { getContactDetailById } from '@utils/contact-details'; +import content from '@content/content'; +import { + makeEmailContactDetail, + makeSmsContactDetail, +} from '@testhelpers/helpers'; + +const { pageTitle, bannerHeading, sendLink, templatesLink } = + content.pages.emailAddressConfirmedPage; + +jest.mock('next/navigation'); +jest.mock('@utils/contact-details'); +jest.mock('@utils/server-features'); + +const EMAIL_CONTACT_DETAIL = makeEmailContactDetail(); + +beforeEach(() => { + jest.resetAllMocks(); + jest.mocked(serverIsFeatureEnabled).mockResolvedValue(true); + jest.mocked(getContactDetailById).mockResolvedValue(EMAIL_CONTACT_DETAIL); +}); + +it('should generate correct metadata', async () => { + const metadata = await generateMetadata(); + + expect(metadata).toEqual({ title: pageTitle }); +}); + +describe('renders page correctly', () => { + it('should render banner heading', async () => { + const { getByTestId } = render( + await Page({ + params: Promise.resolve({ contactDetailId: 'contact-123' }), + searchParams: Promise.resolve({}), + }) + ); + + expect(getByTestId('banner-heading')).toHaveTextContent(bannerHeading); + }); + + it('should render email address from contact detail', async () => { + const { getByText } = render( + await Page({ + params: Promise.resolve({ contactDetailId: 'contact-123' }), + searchParams: Promise.resolve({}), + }) + ); + + expect(getByText('TEST@example.com')).toBeInTheDocument(); + }); + + it('should render back link to send-test-email when templateId is provided', async () => { + const templateId = 'template-456'; + const { getByTestId } = render( + await Page({ + params: Promise.resolve({ contactDetailId: 'contact-123' }), + searchParams: Promise.resolve({ templateId }), + }) + ); + + expect(getByTestId('back-link-bottom')).toHaveAttribute( + 'href', + `/send-test-email/${templateId}` + ); + expect(getByTestId('back-link-bottom')).toHaveTextContent(sendLink.text); + }); + + it('should render back link to message-templates when no templateId', async () => { + const { getByTestId } = render( + await Page({ + params: Promise.resolve({ contactDetailId: 'contact-123' }), + searchParams: Promise.resolve({}), + }) + ); + + expect(getByTestId('back-link-bottom')).toHaveAttribute( + 'href', + templatesLink.href + ); + expect(getByTestId('back-link-bottom')).toHaveTextContent( + templatesLink.text + ); + }); + + it('should match snapshot without templateId', async () => { + const { asFragment } = render( + await Page({ + params: Promise.resolve({ contactDetailId: 'contact-123' }), + searchParams: Promise.resolve({}), + }) + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + it('should match snapshot with templateId', async () => { + const { asFragment } = render( + await Page({ + params: Promise.resolve({ contactDetailId: 'contact-123' }), + searchParams: Promise.resolve({ templateId: 'template-456' }), + }) + ); + + expect(asFragment()).toMatchSnapshot(); + }); +}); + +describe('redirects', () => { + it('should redirect to message-templates when digitalProofingEmail is disabled', async () => { + jest.mocked(serverIsFeatureEnabled).mockResolvedValueOnce(false); + + await Page({ + params: Promise.resolve({ contactDetailId: 'contact-123' }), + searchParams: Promise.resolve({}), + }); + + expect(redirect).toHaveBeenCalledWith('/message-templates', 'replace'); + expect(getContactDetailById).not.toHaveBeenCalled(); + }); + + it('should redirect to message-templates when contact detail is not found', async () => { + jest.mocked(getContactDetailById).mockResolvedValueOnce(undefined); + + await Page({ + params: Promise.resolve({ contactDetailId: 'not-found' }), + searchParams: Promise.resolve({}), + }); + + expect(redirect).toHaveBeenCalledWith('/message-templates', 'replace'); + }); + + it('should redirect to message-templates when contact detail is not verified', async () => { + jest + .mocked(getContactDetailById) + .mockResolvedValueOnce( + makeEmailContactDetail({ status: 'PENDING_VERIFICATION' }) + ); + + await Page({ + params: Promise.resolve({ contactDetailId: 'not-found' }), + searchParams: Promise.resolve({}), + }); + + expect(redirect).toHaveBeenCalledWith('/message-templates', 'replace'); + }); + + it('should redirect to message-templates when contact detail type is not EMAIL', async () => { + jest + .mocked(getContactDetailById) + .mockResolvedValueOnce(makeSmsContactDetail()); + + await Page({ + params: Promise.resolve({ contactDetailId: 'contact-123' }), + searchParams: Promise.resolve({}), + }); + + expect(redirect).toHaveBeenCalledWith('/message-templates', 'replace'); + }); +}); diff --git a/frontend/src/app/email-address-confirmed/[contactDetailId]/page.tsx b/frontend/src/app/email-address-confirmed/[contactDetailId]/page.tsx new file mode 100644 index 000000000..2ac1259af --- /dev/null +++ b/frontend/src/app/email-address-confirmed/[contactDetailId]/page.tsx @@ -0,0 +1,90 @@ +'use server'; + +import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain'; +import content from '@content/content'; +import { NHSNotifyContainer } from '@layouts/container/container'; +import { ContentRenderer } from '@molecules/ContentRenderer/ContentRenderer'; +import { getContactDetailById } from '@utils/contact-details'; +import { interpolate } from '@utils/interpolate'; +import { $ContactDetailsSearchParams } from '@utils/schemas'; +import { serverIsFeatureEnabled } from '@utils/server-features'; +import { Metadata } from 'next'; +import Link from 'next/link'; +import { redirect, RedirectType } from 'next/navigation'; +import type { ContactDetailPageProps } from 'nhs-notify-web-template-management-utils'; + +const { pageTitle, bannerHeading, bannerBody, sendLink, templatesLink } = + content.pages.emailAddressConfirmedPage; + +export async function generateMetadata(): Promise { + return { + title: pageTitle, + }; +} + +const EmailAddressConfirmedPage = async ({ + params, + searchParams, +}: ContactDetailPageProps) => { + const isDigitalProofingEmailEnabled = await serverIsFeatureEnabled( + 'digitalProofingEmail' + ); + + if (!isDigitalProofingEmailEnabled) { + return redirect(`/message-templates`, RedirectType.replace); + } + + const { contactDetailId } = await params; + const { templateId } = $ContactDetailsSearchParams.parse(await searchParams); + + const contactDetail = await getContactDetailById(contactDetailId); + + if ( + !contactDetail || + contactDetail.status !== 'VERIFIED' || + contactDetail.type !== 'EMAIL' + ) { + return redirect('/message-templates', RedirectType.replace); + } + + const backLink = templateId ? sendLink : templatesLink; + + return ( + + +
+
+
+

+ {bannerHeading} +

+
+ +
+
+ { + + {backLink.text} + + } +
+
+
+
+ ); +}; + +export default EmailAddressConfirmedPage; diff --git a/frontend/src/content/content.ts b/frontend/src/content/content.ts index 6fa1f6301..2c0afaee3 100644 --- a/frontend/src/content/content.ts +++ b/frontend/src/content/content.ts @@ -1542,6 +1542,29 @@ const previewDigitalTemplate = { } satisfies PreviewDigitalContent, }; +const emailAddressConfirmedPage = { + pageTitle: generatePageTitle('Email address confirmed'), + bannerHeading: 'Email address confirmed', + bannerBody: [ + { + type: 'text', + text: 'You can now send test emails to **{{contactDetail}}**', + }, + { + type: 'text', + text: 'This email address will only be available for you to use. If someone else needs to send test emails to it, they must add it to their own account.', + }, + ] satisfies ContentBlock[], + sendLink: { + href: '/send-test-email/{{templateId}}', + text: 'Continue to send a test email', + }, + templatesLink: { + href: '/message-templates', + text: backToAllTemplates, + }, +}; + const sendTestMessageIntroParagraphs: Record< DigitalTemplateType, ContentBlock[] @@ -2651,6 +2674,7 @@ const content = { editMessagePlan, editTemplateCampaignPage, editTemplateNamePage, + emailAddressConfirmedPage, enterSecurityCode, error404, getReadyToApproveLetterTemplate, @@ -2673,9 +2697,9 @@ const content = { sendTestEmailMessagePage, sendTestNhsAppMessagePage, sendTestSmsMessagePage, + submitLetterTemplate: submitLetterTemplatePage, testEmailMessageSentPage, testNHSAppMessageSentPage, - submitLetterTemplate: submitLetterTemplatePage, uploadDocxLetterTemplatePage, }, }; diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index 4210bbfe3..70e7f46a0 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -19,6 +19,7 @@ const protectedPaths = [ /^\/edit-template-campaign\/[^/]+$/, /^\/edit-template-name\/[^/]+$/, /^\/edit-text-message-template\/[^/]+$/, + /^\/email-address-confirmed\/[^/]+$/, /^\/email-template-submitted\/[^/]+$/, /^\/enter-security-code\/[^/]+$/, /^\/files\/.+$/, diff --git a/tests/test-team/pages/contact-details/index.ts b/tests/test-team/pages/contact-details/index.ts index 945f8e116..19479cc46 100644 --- a/tests/test-team/pages/contact-details/index.ts +++ b/tests/test-team/pages/contact-details/index.ts @@ -1,2 +1,3 @@ export * from './template-mgmt-add-email-address-page'; +export * from './template-mgmt-email-address-confirmed-page'; export * from './template-mgmt-enter-security-code-page'; diff --git a/tests/test-team/pages/contact-details/template-mgmt-email-address-confirmed-page.ts b/tests/test-team/pages/contact-details/template-mgmt-email-address-confirmed-page.ts new file mode 100644 index 000000000..3ce2481f9 --- /dev/null +++ b/tests/test-team/pages/contact-details/template-mgmt-email-address-confirmed-page.ts @@ -0,0 +1,27 @@ +import { Page } from '@playwright/test'; +import { TemplateMgmtBasePage } from '../template-mgmt-base-page'; + +export class TemplateMgmtEmailAddressConfirmedPage extends TemplateMgmtBasePage { + static readonly pathTemplate = '/email-address-confirmed/:contactDetailId'; + + public static readonly urlRegexp = new RegExp( + /\/templates\/email-address-confirmed\/([\dA-Fa-f-]+)$/ + ); + + public readonly pageHeading = this.page.locator( + '[data-testid="banner-heading"]' + ); + + public readonly emailAddress = this.page + .locator('.nhsuk-panel__body p') + .first() + .locator('strong'); + + constructor(page: Page) { + super(page); + } + + get contactDetailId() { + return this.getPathParametersFromCurrentPageUrl()['contactDetailId']; + } +} diff --git a/tests/test-team/template-mgmt-accessibility/contact-details.accessibility.spec.ts b/tests/test-team/template-mgmt-accessibility/contact-details.accessibility.spec.ts index 5e2c7ec44..35bd23f6e 100644 --- a/tests/test-team/template-mgmt-accessibility/contact-details.accessibility.spec.ts +++ b/tests/test-team/template-mgmt-accessibility/contact-details.accessibility.spec.ts @@ -7,15 +7,19 @@ import { ContactDetailHelper } from 'helpers/db/contact-details-helper'; import { makeUnverifiedEmailContactDetail, makeUnverifiedSmsContactDetail, + makeVerifiedEmailContactDetail, } from 'helpers/factories/contact-details-factory'; import { TemplateMgmtAddEmailAddressPage, + TemplateMgmtEmailAddressConfirmedPage, TemplateMgmtEnterSecurityCodePage, } from 'pages/contact-details'; const contactDetailIds = { UNVERIFIED_EMAIL: randomUUID(), UNVERIFIED_SMS_ERROR: randomUUID(), + VERIFIED_EMAIL: randomUUID(), + VERIFIED_SMS: randomUUID(), }; const templateId = randomUUID(); @@ -43,7 +47,17 @@ test.beforeAll(async () => { value: '07306542878', }); - await contactDetailHelper.seed([contactDetailEmail, contactDetailSmsError]); + const verifiedEmail = makeVerifiedEmailContactDetail({ + owner: digitalProofingUser.internalUserId, + id: contactDetailIds.VERIFIED_EMAIL, + value: 'verified@nhs.net', + }); + + await contactDetailHelper.seed([ + contactDetailEmail, + contactDetailSmsError, + verifiedEmail, + ]); }); test.afterAll(async () => { @@ -84,4 +98,19 @@ test.describe.serial('Contact Details', () => { }, } )); + + test('Email address confirmed', async ({ page, analyze }) => + analyze( + new TemplateMgmtEmailAddressConfirmedPage(page).setPathParam( + 'contactDetailId', + contactDetailIds.VERIFIED_EMAIL + ) + )); + + test('Email address confirmed - with templateId', async ({ page, analyze }) => + analyze( + new TemplateMgmtEmailAddressConfirmedPage(page) + .setPathParam('contactDetailId', contactDetailIds.VERIFIED_EMAIL) + .setSearchParam('templateId', templateId) + )); }); diff --git a/tests/test-team/template-mgmt-accessibility/route-coverage.accessibility.spec.ts b/tests/test-team/template-mgmt-accessibility/route-coverage.accessibility.spec.ts index 6a46591be..0ad25fc24 100644 --- a/tests/test-team/template-mgmt-accessibility/route-coverage.accessibility.spec.ts +++ b/tests/test-team/template-mgmt-accessibility/route-coverage.accessibility.spec.ts @@ -70,8 +70,11 @@ import { } from 'pages/letter'; // Contact details pages -import { TemplateMgmtEnterSecurityCodePage } from 'pages/contact-details'; -import { TemplateMgmtAddEmailAddressPage } from 'pages/contact-details'; +import { + TemplateMgmtAddEmailAddressPage, + TemplateMgmtEmailAddressConfirmedPage, + TemplateMgmtEnterSecurityCodePage, +} from 'pages/contact-details'; // Routing (message plan) pages import { @@ -198,6 +201,7 @@ const allPages: (typeof TemplateMgmtBasePage)[] = [ // Contact Details TemplateMgmtAddEmailAddressPage, + TemplateMgmtEmailAddressConfirmedPage, TemplateMgmtEnterSecurityCodePage, ]; diff --git a/tests/test-team/template-mgmt-component-tests/contact-details-component/template-mgmt-email-address-confirmed-page.contact-details-component.spec.ts b/tests/test-team/template-mgmt-component-tests/contact-details-component/template-mgmt-email-address-confirmed-page.contact-details-component.spec.ts new file mode 100644 index 000000000..c4019d131 --- /dev/null +++ b/tests/test-team/template-mgmt-component-tests/contact-details-component/template-mgmt-email-address-confirmed-page.contact-details-component.spec.ts @@ -0,0 +1,129 @@ +import { test, expect } from '@playwright/test'; +import { randomUUID } from 'node:crypto'; +import { TestUser, testUsers } from '../../helpers/auth/cognito-auth-helper'; +import { getTestContext } from '../../helpers/context/context'; +import { loginAsUser } from '../../helpers/auth/login-as-user'; +import { ContactDetailHelper } from '../../helpers/db/contact-details-helper'; +import { makeVerifiedEmailContactDetail } from '../../helpers/factories/contact-details-factory'; +import { + assertAndClickBackLinkBottom, + assertBackLinkTopNotPresent, + assertFooterLinks, + assertHeaderLogoLink, + assertSignOutLink, + assertSkipToMainContent, +} from '../../helpers/template-mgmt-common.steps'; +import { TemplateMgmtEmailAddressConfirmedPage } from '../../pages/contact-details'; + +const templateId = randomUUID(); + +const contactDetailIds = { + VERIFIED_EMAIL: randomUUID(), + VERIFIED_EMAIL_WITH_TEMPLATE: randomUUID(), +}; + +test.describe('Email Address Confirmed Page', () => { + let digitalProofingUser: TestUser; + const contactDetailHelper = new ContactDetailHelper(); + + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeAll(async () => { + const context = getTestContext(); + digitalProofingUser = await context.auth.getTestUser( + testUsers.UserDigitalProofingEnabled.userId + ); + + const verifiedEmail = makeVerifiedEmailContactDetail({ + owner: digitalProofingUser.internalUserId, + id: contactDetailIds.VERIFIED_EMAIL, + value: 'confirmed@nhs.net', + }); + + const verifiedEmailWithTemplate = makeVerifiedEmailContactDetail({ + owner: digitalProofingUser.internalUserId, + id: contactDetailIds.VERIFIED_EMAIL_WITH_TEMPLATE, + value: 'confirmed-template@nhs.net', + }); + + await contactDetailHelper.seed([verifiedEmail, verifiedEmailWithTemplate]); + }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(digitalProofingUser, page); + }); + + test.afterAll(async () => { + await contactDetailHelper.cleanup(); + }); + + test('common page tests - with templateId', async ({ page, baseURL }) => { + const confirmedPage = new TemplateMgmtEmailAddressConfirmedPage(page) + .setPathParam( + 'contactDetailId', + contactDetailIds.VERIFIED_EMAIL_WITH_TEMPLATE + ) + .setSearchParam('templateId', templateId); + + await confirmedPage.loadPage(); + await expect(page).toHaveURL(confirmedPage.getUrl()); + await expect(confirmedPage.pageHeading).toHaveText( + 'Email address confirmed' + ); + + const props = { page: confirmedPage, baseURL }; + + await assertSkipToMainContent(props); + await assertHeaderLogoLink(props); + await assertSignOutLink(props); + await assertFooterLinks(props); + await assertBackLinkTopNotPresent(props); + await assertAndClickBackLinkBottom({ + ...props, + expectedUrl: `templates/send-test-email/${templateId}`, + }); + }); + + test('common page tests - without templateId', async ({ page, baseURL }) => { + const confirmedPage = new TemplateMgmtEmailAddressConfirmedPage( + page + ).setPathParam('contactDetailId', contactDetailIds.VERIFIED_EMAIL); + + await confirmedPage.loadPage(); + await expect(page).toHaveURL(confirmedPage.getUrl()); + await expect(confirmedPage.pageHeading).toHaveText( + 'Email address confirmed' + ); + + const props = { page: confirmedPage, baseURL }; + + await assertBackLinkTopNotPresent(props); + await assertAndClickBackLinkBottom({ + ...props, + expectedUrl: `templates/message-templates`, + }); + }); + + test('should display confirmed email address', async ({ page }) => { + const confirmedPage = new TemplateMgmtEmailAddressConfirmedPage( + page + ).setPathParam('contactDetailId', contactDetailIds.VERIFIED_EMAIL); + + await confirmedPage.loadPage(); + + await expect(confirmedPage.emailAddress).toHaveText('confirmed@nhs.net'); + }); + + test('should redirect to message-templates when contact detail does not exist', async ({ + baseURL, + page, + }) => { + const confirmedPage = new TemplateMgmtEmailAddressConfirmedPage( + page + ).setPathParam('contactDetailId', randomUUID()); + + await confirmedPage.loadPage(); + + await expect(page).toHaveURL(`${baseURL}/templates/message-templates`); + }); +}); diff --git a/tests/test-team/template-mgmt-component-tests/template-component/template-protected-routes.template-component.spec.ts b/tests/test-team/template-mgmt-component-tests/template-component/template-protected-routes.template-component.spec.ts index 13c15e6cf..087d4dee3 100644 --- a/tests/test-team/template-mgmt-component-tests/template-component/template-protected-routes.template-component.spec.ts +++ b/tests/test-team/template-mgmt-component-tests/template-component/template-protected-routes.template-component.spec.ts @@ -74,7 +74,10 @@ import { TemplateMgmtUploadOtherLanguageLetterTemplatePage } from 'pages/letter/ import { TemplateMgmtUploadStandardEnglishLetterTemplatePage } from 'pages/letter/template-mgmt-upload-standard-english-letter-template-page'; import { TemplateMgmtPreviewApprovedLetterPage } from 'pages/letter/template-mgmt-preview-approved-letter-page'; import { TemplateMgmtLetterTemplateApprovedPage } from 'pages/letter'; -import { TemplateMgmtEnterSecurityCodePage } from 'pages/contact-details'; +import { + TemplateMgmtEmailAddressConfirmedPage, + TemplateMgmtEnterSecurityCodePage, +} from 'pages/contact-details'; import { TemplateMgmtAddEmailAddressPage } from 'pages/contact-details/template-mgmt-add-email-address-page'; import { TemplateMgmtTestEmailMessageSentPage } from 'pages/email/template-mgmt-test-email-message-sent-page'; import { TemplateMgmtTestNhsAppMessageSentPage } from 'pages/nhs-app'; @@ -111,6 +114,7 @@ const protectedPages = [ RoutingReviewAndMoveToProductionPreviewLetterTemplatePage, TemplateMgmtAddEmailAddressPage, TemplateMgmtChoosePage, + TemplateMgmtEmailAddressConfirmedPage, TemplateMgmtChoosePrintingAndPostagePage, TemplateMgmtCopyPage, TemplateMgmtCreateEmailPage,