diff --git a/.tool-versions b/.tool-versions
index 58b6eff99..d1b730e3f 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,4 +1,5 @@
act 0.2.64
+direnv 2.32.1
gitleaks 8.24.0
jq 1.6
nodejs 22.22.0
diff --git a/frontend/src/__tests__/app/choose-printing-and-postage/[templateId]/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/choose-printing-and-postage/[templateId]/__snapshots__/page.test.tsx.snap
new file mode 100644
index 000000000..dcbffcbf7
--- /dev/null
+++ b/frontend/src/__tests__/app/choose-printing-and-postage/[templateId]/__snapshots__/page.test.tsx.snap
@@ -0,0 +1,712 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`valid template matches snapshot on initial render 1`] = `
+
+
+
+`;
+
+exports[`valid template renders errors when no variant is selected and error state is returned 1`] = `
+
+
+
+
+
+ There is a problem
+
+
+ You have not chosen a printing and postage option
+
+
+
+
+
+
+
+`;
diff --git a/frontend/src/__tests__/app/choose-printing-and-postage/[templateId]/page.test.tsx b/frontend/src/__tests__/app/choose-printing-and-postage/[templateId]/page.test.tsx
new file mode 100644
index 000000000..bbccf67f9
--- /dev/null
+++ b/frontend/src/__tests__/app/choose-printing-and-postage/[templateId]/page.test.tsx
@@ -0,0 +1,421 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { redirect, RedirectType } from 'next/navigation';
+import type {
+ LetterVariant,
+ TemplateDto,
+} from 'nhs-notify-web-template-management-types';
+import { fetchClient } from '@utils/server-features';
+import { getLetterVariantsForTemplate, getTemplate } from '@utils/form-actions';
+import { verifyFormCsrfToken } from '@utils/csrf-utils';
+import { choosePrintingAndPostage } from '@app/choose-printing-and-postage/[templateId]/server-action';
+import Page, {
+ metadata,
+} from '@app/choose-printing-and-postage/[templateId]/page';
+
+jest.mock('next/navigation');
+jest.mock('@utils/server-features');
+jest.mock('@utils/form-actions');
+jest.mock('@app/choose-printing-and-postage/[templateId]/server-action');
+jest.mock('@utils/csrf-utils');
+
+const mockTemplate: TemplateDto = {
+ id: 'template-123',
+ name: 'Test Letter Template',
+ templateType: 'LETTER',
+ letterVersion: 'AUTHORING',
+ templateStatus: 'NOT_YET_SUBMITTED',
+ lockNumber: 5,
+ language: 'en',
+ letterType: 'x0',
+ createdAt: '2026-01-01T00:00:00.000Z',
+ updatedAt: '2026-01-01T00:00:00.000Z',
+ clientId: 'client-123',
+ letterVariantId: 'variant-1',
+ files: {
+ docxTemplate: {
+ currentVersion: 'version',
+ fileName: 'name.pdf',
+ virusScanStatus: 'PASSED',
+ },
+ initialRender: {
+ status: 'PENDING',
+ requestedAt: '2026-01-01T00:00:00.000Z',
+ },
+ },
+};
+
+const mockVariants: LetterVariant[] = [
+ {
+ id: 'variant-1',
+ name: 'First Class Option',
+ bothSides: true,
+ dispatchTime: 'standard',
+ envelopeSize: 'C4',
+ maxSheets: 4,
+ postage: 'first class',
+ printColour: 'colour',
+ sheetSize: 'A4',
+ status: 'PROD',
+ type: 'STANDARD',
+ },
+ {
+ id: 'variant-2',
+ name: 'Economy Option',
+ bothSides: false,
+ dispatchTime: 'standard',
+ envelopeSize: 'C5',
+ maxSheets: 2,
+ postage: 'economy',
+ printColour: 'black',
+ sheetSize: 'A4',
+ status: 'PROD',
+ type: 'STANDARD',
+ },
+];
+
+const validSearchParams = Promise.resolve({ lockNumber: '7' });
+
+beforeEach(() => {
+ jest.resetAllMocks();
+ jest.mocked(choosePrintingAndPostage).mockResolvedValue({
+ errorState: {
+ formErrors: [],
+ fieldErrors: {},
+ },
+ fields: {},
+ });
+ jest.mocked(fetchClient).mockResolvedValue({
+ features: {
+ letterAuthoring: true,
+ },
+ });
+ jest.mocked(getTemplate).mockResolvedValue(mockTemplate);
+ jest.mocked(getLetterVariantsForTemplate).mockResolvedValue(mockVariants);
+ jest.mocked(verifyFormCsrfToken).mockResolvedValue(true);
+});
+
+test('metadata', () => {
+ expect(metadata).toEqual({
+ title: 'Choose a printing and postage option - NHS Notify',
+ });
+});
+
+describe('template does not exist', () => {
+ beforeEach(() => {
+ jest.mocked(getTemplate).mockResolvedValue(undefined);
+ });
+
+ it('redirects to invalid template page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/invalid-template',
+ RedirectType.replace
+ );
+ });
+});
+
+describe('template is not a letter', () => {
+ beforeEach(() => {
+ jest.mocked(getTemplate).mockResolvedValue({
+ id: 'template-123',
+ name: 'Email Template',
+ templateType: 'EMAIL',
+ templateStatus: 'NOT_YET_SUBMITTED',
+ lockNumber: 5,
+ message: 'Test message',
+ subject: 'Test subject',
+ createdAt: '2026-01-01T00:00:00.000Z',
+ updatedAt: '2026-01-01T00:00:00.000Z',
+ clientId: 'client-123',
+ });
+ });
+
+ it('redirects to message templates page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/message-templates',
+ RedirectType.replace
+ );
+ });
+});
+
+describe('letter version is not AUTHORING', () => {
+ beforeEach(() => {
+ jest.mocked(getTemplate).mockResolvedValue({
+ id: 'template-123',
+ name: 'PDF Letter Template',
+ templateType: 'LETTER',
+ templateStatus: 'NOT_YET_SUBMITTED',
+ lockNumber: 5,
+ language: 'en',
+ letterType: 'x0',
+ letterVersion: 'PDF',
+ files: {
+ pdfTemplate: {
+ fileName: 'test.pdf',
+ currentVersion: 'v1',
+ virusScanStatus: 'PASSED',
+ },
+ },
+ createdAt: '2026-01-01T00:00:00.000Z',
+ updatedAt: '2026-01-01T00:00:00.000Z',
+ clientId: 'client-123',
+ });
+ });
+
+ it('redirects to preview letter template page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/preview-letter-template/template-123',
+ RedirectType.replace
+ );
+ });
+});
+
+describe('client has letter authoring feature flag disabled', () => {
+ beforeEach(() => {
+ jest.mocked(fetchClient).mockResolvedValue({
+ features: {
+ letterAuthoring: false,
+ },
+ });
+ });
+
+ it('redirects to message templates page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/message-templates',
+ RedirectType.replace
+ );
+ });
+});
+
+describe('template has been submitted', () => {
+ beforeEach(() => {
+ jest.mocked(getTemplate).mockResolvedValue({
+ ...mockTemplate,
+ templateStatus: 'SUBMITTED',
+ });
+ });
+
+ it('redirects to preview submitted letter template page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/preview-submitted-letter-template/template-123',
+ RedirectType.replace
+ );
+ });
+});
+
+describe('lockNumber search parameter validation fails', () => {
+ it('redirects to preview letter template page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: Promise.resolve({ lockNumber: 'invalid-lock-number' }),
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/preview-letter-template/template-123',
+ RedirectType.replace
+ );
+ });
+});
+
+describe('variants is undefined', () => {
+ beforeEach(() => {
+ jest.mocked(getLetterVariantsForTemplate).mockResolvedValue(undefined);
+ });
+
+ it('redirects to preview letter template page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/preview-letter-template/template-123',
+ RedirectType.replace
+ );
+ });
+});
+
+describe('variants is empty array', () => {
+ beforeEach(() => {
+ jest.mocked(getLetterVariantsForTemplate).mockResolvedValue([]);
+ });
+
+ it('redirects to preview letter template page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/preview-letter-template/template-123',
+ RedirectType.replace
+ );
+ });
+});
+
+describe('valid template', () => {
+ it('matches snapshot on initial render', async () => {
+ expect(
+ render(
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
+ ).asFragment()
+ ).toMatchSnapshot();
+ });
+
+ it('submits the form with correct data', async () => {
+ const user = userEvent.setup();
+
+ render(
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
+ );
+
+ const variantRadio = screen.getByRole('radio', { name: 'Economy Option' });
+ await user.click(variantRadio);
+
+ await user.click(screen.getByRole('button', { name: 'Save and continue' }));
+
+ expect(choosePrintingAndPostage).toHaveBeenCalledTimes(1);
+
+ const callArgs = jest.mocked(choosePrintingAndPostage).mock.calls[0];
+ const formData = callArgs[1] as FormData;
+
+ expect(formData.get('letterVariantId')).toBe('variant-2');
+ expect(formData.get('templateId')).toBe('template-123');
+ expect(formData.get('lockNumber')).toBe('7');
+
+ expect(
+ screen.queryByRole('alert', { name: 'There is a problem' })
+ ).not.toBeInTheDocument();
+ });
+
+ it('renders errors when no variant is selected and error state is returned', async () => {
+ jest.mocked(choosePrintingAndPostage).mockResolvedValue({
+ errorState: {
+ formErrors: [],
+ fieldErrors: {
+ letterVariantId: ['Choose a printing and postage option'],
+ },
+ },
+ fields: {},
+ });
+
+ const user = userEvent.setup();
+
+ const page = render(
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
+ );
+
+ await user.click(screen.getByRole('button', { name: 'Save and continue' }));
+
+ expect(choosePrintingAndPostage).toHaveBeenCalledTimes(1);
+
+ expect(
+ await screen.findByRole('alert', { name: 'There is a problem' })
+ ).toBeInTheDocument();
+
+ expect(page.asFragment()).toMatchSnapshot();
+ });
+
+ it('displays the back link with correct href', async () => {
+ render(
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
+ );
+
+ const backLink = screen.getByRole('link', { name: 'Go back' });
+ expect(backLink).toHaveAttribute(
+ 'href',
+ '/preview-letter-template/template-123'
+ );
+ });
+
+ it('displays variant details correctly', async () => {
+ render(
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
+ );
+
+ expect(
+ screen.getByRole('radio', { name: 'First Class Option' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('radio', { name: 'Economy Option' })
+ ).toBeInTheDocument();
+
+ for (const [index, variant] of mockVariants.entries()) {
+ expect(screen.getAllByText(/Sheet size/)[index]).toHaveTextContent(
+ variant.sheetSize
+ );
+ expect(
+ screen.getAllByText(/Maximum number of sheets/)[index]
+ ).toHaveTextContent(String(variant.maxSheets));
+ expect(
+ screen.getAllByText(/Print on both sides/)[index]
+ ).toHaveTextContent(variant.bothSides ? 'yes' : 'no');
+ expect(screen.getAllByText(/Print colour/)[index]).toHaveTextContent(
+ variant.printColour
+ );
+ expect(screen.getAllByText(/Envelope size/)[index]).toHaveTextContent(
+ variant.envelopeSize
+ );
+ expect(screen.getAllByText(/Dispatch time/)[index]).toHaveTextContent(
+ variant.dispatchTime
+ );
+ expect(screen.getAllByText(/Postage/)[index]).toHaveTextContent(
+ variant.postage
+ );
+ }
+ });
+
+ it('pre-selects the current letterVariantId from template', async () => {
+ render(
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
+ );
+
+ const standardOptionRadio = screen.getByRole('radio', {
+ name: 'First Class Option',
+ });
+ expect(standardOptionRadio).toBeChecked();
+ });
+});
diff --git a/frontend/src/__tests__/app/choose-printing-and-postage/[templateId]/server-action.test.ts b/frontend/src/__tests__/app/choose-printing-and-postage/[templateId]/server-action.test.ts
new file mode 100644
index 000000000..4fd67bd45
--- /dev/null
+++ b/frontend/src/__tests__/app/choose-printing-and-postage/[templateId]/server-action.test.ts
@@ -0,0 +1,112 @@
+import { redirect } from 'next/navigation';
+import { NextRedirectError } from '@testhelpers/next-redirect';
+import { patchTemplate } from '@utils/form-actions';
+import { choosePrintingAndPostage } from '@app/choose-printing-and-postage/[templateId]/server-action';
+
+jest.mock('next/navigation');
+jest.mocked(redirect).mockImplementation((url) => {
+ throw new NextRedirectError(url);
+});
+
+jest.mock('@utils/form-actions');
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe('choosePrintingAndPostage', () => {
+ it('patches the template and redirects when all fields are valid', async () => {
+ const formData = new FormData();
+ formData.append('letterVariantId', 'variant-456');
+ formData.append('templateId', 'template-123');
+ formData.append('lockNumber', '5');
+
+ await expect(choosePrintingAndPostage({}, formData)).rejects.toMatchObject({
+ message: 'NEXT_REDIRECT',
+ url: '/preview-letter-template/template-123',
+ });
+
+ expect(patchTemplate).toHaveBeenCalledWith(
+ 'template-123',
+ { letterVariantId: 'variant-456' },
+ 5
+ );
+ });
+
+ it('returns validation error when letterVariantId is empty', async () => {
+ const formData = new FormData();
+ formData.append('letterVariantId', '');
+ formData.append('templateId', 'template-123');
+ formData.append('lockNumber', '5');
+
+ const result = await choosePrintingAndPostage({}, formData);
+
+ expect(result.errorState).toEqual({
+ formErrors: [],
+ fieldErrors: {
+ letterVariantId: ['Choose a printing and postage option'],
+ },
+ });
+ });
+
+ it('returns validation error when letterVariantId is missing', async () => {
+ const formData = new FormData();
+ formData.append('templateId', 'template-123');
+ formData.append('lockNumber', '5');
+
+ const result = await choosePrintingAndPostage({}, formData);
+
+ expect(result.errorState).toEqual({
+ formErrors: [],
+ fieldErrors: {
+ letterVariantId: ['Choose a printing and postage option'],
+ },
+ });
+ });
+
+ it('returns validation error when templateId is missing', async () => {
+ const formData = new FormData();
+ formData.append('letterVariantId', 'variant-456');
+ formData.append('lockNumber', '5');
+
+ const result = await choosePrintingAndPostage({}, formData);
+
+ expect(result.errorState).toEqual({
+ formErrors: [],
+ fieldErrors: {
+ templateId: [expect.any(String)],
+ },
+ });
+ });
+
+ it('returns validation error when lockNumber is missing', async () => {
+ const formData = new FormData();
+ formData.append('letterVariantId', 'variant-456');
+ formData.append('templateId', 'template-123');
+
+ const result = await choosePrintingAndPostage({}, formData);
+
+ expect(result.errorState).toEqual({
+ formErrors: [],
+ fieldErrors: {
+ lockNumber: [expect.any(String)],
+ },
+ });
+ });
+
+ it('returns validation error when lockNumber is invalid', async () => {
+ const formData = new FormData();
+ formData.append('letterVariantId', 'variant-456');
+ formData.append('templateId', 'template-123');
+ formData.append('lockNumber', 'invalid');
+
+ const result = await choosePrintingAndPostage({}, formData);
+
+ expect(result.errorState).toEqual({
+ formErrors: [],
+ fieldErrors: {
+ lockNumber: [expect.any(String)],
+ },
+ });
+ });
+});
diff --git a/frontend/src/__tests__/app/edit-template-campaign/[templateId]/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/edit-template-campaign/[templateId]/__snapshots__/page.test.tsx.snap
index 84294951f..3071625cb 100644
--- a/frontend/src/__tests__/app/edit-template-campaign/[templateId]/__snapshots__/page.test.tsx.snap
+++ b/frontend/src/__tests__/app/edit-template-campaign/[templateId]/__snapshots__/page.test.tsx.snap
@@ -41,7 +41,7 @@ exports[`valid template matches snapshot on initial render 1`] = `
name="lockNumber"
readonly=""
type="hidden"
- value="5"
+ value="7"
/>
{
jest.resetAllMocks();
jest.mocked(editTemplateCampaign).mockResolvedValue({});
@@ -67,7 +69,10 @@ describe('template does not exist', () => {
});
it('redirects to invalid template page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/invalid-template',
@@ -93,7 +98,10 @@ describe('template is not a letter', () => {
});
it('redirects to message templates page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/message-templates',
@@ -127,7 +135,10 @@ describe('letter version is not AUTHORING', () => {
});
it('redirects to preview letter template page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/preview-letter-template/template-123',
@@ -147,7 +158,10 @@ describe('client has letter authoring feature flag disabled', () => {
});
it('redirects to message templates page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/message-templates',
@@ -165,7 +179,10 @@ describe('template has been submitted', () => {
});
it('redirects to preview submitted letter template page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/preview-submitted-letter-template/template-123',
@@ -185,7 +202,24 @@ describe('client only has one campaign', () => {
});
it('redirects to preview letter template page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/preview-letter-template/template-123',
+ RedirectType.replace
+ );
+ });
+});
+
+describe('lockNumber search parameter validation fails', () => {
+ it('redirects to preview letter template page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: Promise.resolve({ lockNumber: 'invalid-lock-number' }),
+ });
expect(redirect).toHaveBeenCalledWith(
'/preview-letter-template/template-123',
@@ -198,7 +232,10 @@ describe('valid template', () => {
it('matches snapshot on initial render', async () => {
expect(
render(
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) })
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
).asFragment()
).toMatchSnapshot();
});
@@ -207,7 +244,10 @@ describe('valid template', () => {
const user = userEvent.setup();
render(
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) })
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
);
const campaignSelect = screen.getByLabelText('Edit template campaign');
@@ -225,7 +265,7 @@ describe('valid template', () => {
expect(formData.get('campaignId')).toBe('Campaign 2');
expect(formData.get('templateId')).toBe('template-123');
- expect(formData.get('lockNumber')).toBe('5');
+ expect(formData.get('lockNumber')).toBe('7');
expect(
screen.queryByRole('alert', { name: 'There is a problem' })
@@ -244,7 +284,10 @@ describe('valid template', () => {
const user = userEvent.setup();
const page = render(
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) })
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
);
const campaignSelect = screen.getByLabelText('Edit template campaign');
@@ -263,7 +306,10 @@ describe('valid template', () => {
it('displays the cancel link with correct href', async () => {
render(
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) })
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
);
const cancelLink = screen.getByRole('link', { name: 'Go back' });
diff --git a/frontend/src/__tests__/app/edit-template-name/[templateId]/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/edit-template-name/[templateId]/__snapshots__/page.test.tsx.snap
index 25706850d..fe3d1460e 100644
--- a/frontend/src/__tests__/app/edit-template-name/[templateId]/__snapshots__/page.test.tsx.snap
+++ b/frontend/src/__tests__/app/edit-template-name/[templateId]/__snapshots__/page.test.tsx.snap
@@ -41,7 +41,7 @@ exports[`valid template matches snapshot on initial render 1`] = `
name="lockNumber"
readonly=""
type="hidden"
- value="5"
+ value="7"
/>
{
jest.resetAllMocks();
jest.mocked(editTemplateName).mockResolvedValue({});
@@ -67,7 +69,10 @@ describe('template does not exist', () => {
});
it('redirects to invalid template page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/invalid-template',
@@ -93,7 +98,10 @@ describe('template is not a letter', () => {
});
it('redirects to message templates page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/message-templates',
@@ -127,7 +135,10 @@ describe('letter version is not AUTHORING', () => {
});
it('redirects to preview letter template page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/preview-letter-template/template-123',
@@ -147,7 +158,10 @@ describe('client has letter authoring feature flag disabled', () => {
});
it('redirects to message templates page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/message-templates',
@@ -165,7 +179,10 @@ describe('template has been submitted', () => {
});
it('redirects to preview submitted letter template page', async () => {
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) });
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ });
expect(redirect).toHaveBeenCalledWith(
'/preview-submitted-letter-template/template-123',
@@ -174,11 +191,28 @@ describe('template has been submitted', () => {
});
});
+describe('lockNumber search parameter validation fails', () => {
+ it('redirects to preview letter template page', async () => {
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: Promise.resolve({ lockNumber: 'invalid-lock-number' }),
+ });
+
+ expect(redirect).toHaveBeenCalledWith(
+ '/preview-letter-template/template-123',
+ RedirectType.replace
+ );
+ });
+});
+
describe('valid template', () => {
it('matches snapshot on initial render', async () => {
expect(
render(
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) })
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
).asFragment()
).toMatchSnapshot();
});
@@ -187,7 +221,10 @@ describe('valid template', () => {
const user = userEvent.setup();
render(
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) })
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
);
const nameInput = screen.getByLabelText('Edit template name');
@@ -207,7 +244,7 @@ describe('valid template', () => {
expect(formData.get('name')).toBe('Updated Template Name');
expect(formData.get('templateId')).toBe('template-123');
- expect(formData.get('lockNumber')).toBe('5');
+ expect(formData.get('lockNumber')).toBe('7');
expect(
screen.queryByRole('alert', { name: 'There is a problem' })
@@ -226,7 +263,10 @@ describe('valid template', () => {
const user = userEvent.setup();
const page = render(
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) })
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
);
const nameInput = screen.getByLabelText('Edit template name');
@@ -245,7 +285,10 @@ describe('valid template', () => {
it('displays the cancel link with correct href', async () => {
render(
- await Page({ params: Promise.resolve({ templateId: 'template-123' }) })
+ await Page({
+ params: Promise.resolve({ templateId: 'template-123' }),
+ searchParams: validSearchParams,
+ })
);
const cancelLink = screen.getByRole('link', { name: 'Go back' });
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 48f920466..af755cecd 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
@@ -414,7 +414,7 @@ exports[`authoring letter template with VALIDATION_FAILED status matches snapsho
>
@@ -563,7 +563,7 @@ exports[`authoring letter template with VALIDATION_FAILED status matches snapsho
>
@@ -970,7 +970,7 @@ exports[`authoring letter template with VALIDATION_FAILED status matches snapsho
>