Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions frontend/src/__tests__/app/add-new-mobile-number/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { mock } from 'jest-mock-extended';
import { useRouter } from 'next/navigation';
import Page, { metadata } from '@app/add-new-mobile-number/page';
import { createContactDetail } from '@utils/contact-details';

jest.mock('next/navigation');
jest.mock('@utils/contact-details');

const mockRouter = mock<ReturnType<typeof useRouter>>();

beforeEach(() => {
jest.resetAllMocks();

jest.mocked(createContactDetail).mockImplementation((input) =>
Promise.resolve({
...input,
id: 'contact-detail-id',
status: 'PENDING_VERIFICATION',
rawValue: input.value,
})
);

jest.mocked(useRouter).mockReturnValue(mockRouter);
});

test('metadata', () => {
expect(metadata).toEqual({
title: 'Add a new mobile number - NHS Notify',
});
});

describe.each([
{
case: 'empty',
searchParams: {},
expectedRedirect: '/enter-security-code/contact-detail-id',
},
{
case: 'with templateId',
searchParams: { templateId: 'template-id' },
expectedRedirect:
'/enter-security-code/contact-detail-id?templateId=template-id',
},
])('search params - $case', ({ searchParams, expectedRedirect }) => {
it('matches initial snapshot', async () => {
expect(
render(
await Page({ searchParams: Promise.resolve(searchParams) })
).asFragment()
).toMatchSnapshot();
});

it('creates new contact detail and redirects to enter security code page', async () => {
const user = userEvent.setup();

render(await Page({ searchParams: Promise.resolve(searchParams) }));

await user.type(screen.getByLabelText('Mobile number'), '07900123456');

await user.click(screen.getByRole('button', { name: 'Continue' }));

expect(createContactDetail).toHaveBeenCalledWith({
type: 'SMS',
value: '07900123456',
});

expect(mockRouter.push).toHaveBeenCalledWith(expectedRedirect);
});
});

describe('form validation', () => {
it.each([
{ case: 'empty', mobile: '' },
{ case: 'invalid', mobile: 'notamobile' },
{ case: 'numberLength', mobile: '123' },
])(
'renders error summary when form is submitted with $case mobile field',
async ({ mobile }) => {
const user = userEvent.setup();

const container = render(
await Page({
searchParams: Promise.resolve({}),
})
);

if (mobile) {
await user.type(screen.getByLabelText('Mobile number'), mobile);
}

await user.click(screen.getByRole('button', { name: 'Continue' }));

await waitFor(async () => {
expect(screen.getByTestId('error-summary')).toBeVisible();
});

expect(container.asFragment()).toMatchSnapshot();

expect(createContactDetail).not.toHaveBeenCalled();
expect(mockRouter.push).not.toHaveBeenCalled();
}
);
});
58 changes: 58 additions & 0 deletions frontend/src/app/add-new-mobile-number/form-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client';

import type { SubmitEvent } from 'react';
import { z } from 'zod/v4';
import { parsePhoneNumber } from 'nhs-notify-backend-client/schemas';
import copy from '@content/content';
import { createContactDetail } from '@utils/contact-details';
import { NHSNotifyClientSideFormSubmitHandler } from '@atoms/NHSNotifyForm/Form';

const content = copy.pages.addMobileNumber;

const $FormSchema = z.object({
mobile: z
.string()
.nonempty(content.form.mobile.errors.empty)
.min(11, content.form.mobile.errors.numberLength)
.pipe(
z
.string()
.refine(
(value) => parsePhoneNumber(value) !== null,
content.form.mobile.errors.invalid
)
),
templateId: z.string().optional(),
});

export const addMobileNumber: NHSNotifyClientSideFormSubmitHandler =
(router, [, setState]) =>
async (event: SubmitEvent) => {
event.preventDefault();

const parseResult = $FormSchema.safeParse(
Object.fromEntries(new FormData(event.target).entries())
);

if (parseResult.error) {
setState({
errorState: z.flattenError(parseResult.error),
});
return;
}

const { mobile, templateId } = parseResult.data;

const { id } = await createContactDetail({
type: 'SMS',
value: mobile,
});

let redirectUrl = `/enter-security-code/${id}`;

if (templateId) {
redirectUrl += `?templateId=${encodeURIComponent(templateId)}`;
}

router.push(redirectUrl);
};
78 changes: 78 additions & 0 deletions frontend/src/app/add-new-mobile-number/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { Metadata } from 'next';
import type { NextJsPageProps } from 'nhs-notify-web-template-management-utils';
import { HintText, Label } from '@atoms/nhsuk-components';
import { NHSNotifyBackLink } from '@atoms/NHSNotifyBackLink/NHSNotifyBackLink';
import { NHSNotifyButton } from '@atoms/NHSNotifyButton/NHSNotifyButton';
import * as NHSNotifyForm from '@atoms/NHSNotifyForm';
import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain';
import addMobileNumberContent from '@content/content';
import { NHSNotifyContainer } from '@layouts/container/container';
import { ContentRenderer } from '@molecules/ContentRenderer/ContentRenderer';
import { NHSNotifyClientSideFormProvider } from '@providers/form-provider';
import { $ContactDetailsSearchParams } from '@utils/schemas';
import { addMobileNumber } from './form-action';

const content = addMobileNumberContent.pages.addMobileNumber;

export const metadata: Metadata = {
title: content.pageTitle,
};

export default async function AddMobileNumberPage(props: NextJsPageProps) {
const { templateId } = $ContactDetailsSearchParams.parse(
await props.searchParams
);

return (
<NHSNotifyContainer>
<NHSNotifyBackLink href={content.backLink.href(templateId)}>
{content.backLink.text}
</NHSNotifyBackLink>
<NHSNotifyMain>
<NHSNotifyClientSideFormProvider>
<NHSNotifyForm.ErrorSummary />
<div className='nhsuk-grid-row'>
<div className='nhsuk-grid-column-two-thirds'>
<h1 className='nhsuk-heading-xl'>{content.pageHeading}</h1>

<ContentRenderer content={content.bodyText} />

<NHSNotifyForm.ClientSideForm onSubmit={addMobileNumber}>
{templateId && (
<input
type='hidden'
name='templateId'
value={templateId}
readOnly
/>
)}

<NHSNotifyForm.FormGroup htmlFor='mobile'>
<Label size='s' htmlFor='mobile'>
{content.form.mobile.label}
</Label>
<HintText>{content.form.mobile.hint}</HintText>

<NHSNotifyForm.ErrorMessage htmlFor='mobile' />
<NHSNotifyForm.Input
type='text'
id='mobile'
name='mobile'
autoComplete='tel'
inputMode='tel'
className='nhsuk-u-width-three-quarters'
/>
</NHSNotifyForm.FormGroup>
<NHSNotifyForm.FormGroup>
<NHSNotifyButton type='submit'>
{content.form.submit.text}
</NHSNotifyButton>
</NHSNotifyForm.FormGroup>
</NHSNotifyForm.ClientSideForm>
</div>
</div>
</NHSNotifyClientSideFormProvider>
</NHSNotifyMain>
</NHSNotifyContainer>
);
}
38 changes: 38 additions & 0 deletions frontend/src/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2441,6 +2441,43 @@ const addEmailAddress = {
},
};

const addMobileNumber = {
pageTitle: generatePageTitle('Add a new mobile number'),
pageHeading: 'Add a new mobile number',
bodyText: [
{
type: 'text',
text: "We'll send a security code to this mobile number to make sure you can access it.",
},
{
type: 'text',
text: 'This mobile number will only be available for you to use. If someone else needs to send tests to it, they must add it to their own account.',
},
] satisfies ContentBlock[],
backLink: {
text: 'Back',
href: (templateId?: string) =>
templateId
? `/send-test-text-message/${templateId}`
: '/message-templates',
},
form: {
mobile: {
label: 'Mobile number',
hint: 'For example, 07700900123. We recommend using a work mobile number',
errors: {
empty: 'Enter a mobile number',
invalid:
'Enter a mobile number in the correct format, like 07700 900123, 07700 900 123 or +447700900123',
numberLength: 'Enter a mobile number between 11 and 13 digits long',
},
},
submit: {
text: 'Continue',
},
},
};

const content = {
global: { mainLayout },
components: {
Expand Down Expand Up @@ -2488,6 +2525,7 @@ const content = {
},
pages: {
addEmailAddress,
addMobileNumber,
chooseDigitalTemplatePage,
chooseLetterTemplatePage,
chooseOtherLanguageLetterTemplate,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getClientIdFromToken } from '@utils/token-utils';

const protectedPaths = [
/^\/add-new-email-address$/,
/^\/add-new-mobile-number$/,
/^\/choose-a-template-type$/,
/^\/choose-printing-and-postage\/[^/]+$/,
/^\/copy-template\/[^/]+$/,
Expand Down
1 change: 1 addition & 0 deletions tests/test-team/pages/contact-details/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './template-mgmt-add-email-address-page';
export * from './template-mgmt-add-mobile-number-page';
export * from './template-mgmt-enter-security-code-page';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { TemplateMgmtBasePage } from '../template-mgmt-base-page';

export class TemplateMgmtAddMobileNumberPage extends TemplateMgmtBasePage {
static readonly pathTemplate = '/add-new-mobile-number';

mobileField = this.page.getByLabel('Mobile number');

continueButton = this.page.getByRole('button', { name: 'Continue' });
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { test } from 'fixtures/accessibility-analyze';
import { TemplateMgmtAddEmailAddressPage } from 'pages/contact-details';
import {
TemplateMgmtAddEmailAddressPage,
TemplateMgmtAddMobileNumberPage,
} from 'pages/contact-details';

test.describe('Contact details pages', () => {
test('Add new email address page', async ({ page, analyze }) =>
Expand All @@ -12,4 +15,17 @@ test.describe('Contact details pages', () => {
await p.errorSummary.isVisible();
},
}));

test.describe('Add new mobile number page', () => {
test('Add new mobile number page', async ({ page, analyze }) =>
analyze(new TemplateMgmtAddMobileNumberPage(page)));

test('Add new mobile number error', async ({ page, analyze }) =>
analyze(new TemplateMgmtAddMobileNumberPage(page), {
beforeAnalyze: async (p) => {
await p.continueButton.click();
await p.errorSummary.isVisible();
},
}));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ import {
} from 'pages/routing';

// Contact Details pages
import { TemplateMgmtAddEmailAddressPage } from 'pages/contact-details';
import {
TemplateMgmtAddEmailAddressPage,
TemplateMgmtAddMobileNumberPage,
} from 'pages/contact-details';

/**
* All page objects that must have accessibility test coverage.
Expand Down Expand Up @@ -195,6 +198,7 @@ const allPages: (typeof TemplateMgmtBasePage)[] = [

// Contact Details
TemplateMgmtAddEmailAddressPage,
TemplateMgmtAddMobileNumberPage,
];

test('all app routes have accessibility test coverage', async () => {
Expand Down
Loading
Loading