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`] = ` + +
+
+
+
+

+ Choose a printing and postage option +

+

+ We only show options that are available for this letter template. Select one option. +

+

+ + Learn about printing and postage (opens in a new tab). + +

+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ Select + + Name + + Details +
+ + +
+ + + + +
+
+ + + + + + +
    +
  • + Sheet size: A4 +
  • +
  • + Maximum number of sheets: 4 +
  • +
  • + Print on both sides: yes +
  • +
  • + Print colour: colour +
  • +
  • + Envelope size: C4 +
  • +
  • + Dispatch time: standard +
  • +
  • + Postage: first class +
  • +
+
+ + +
+ + + + +
+
+ + + + + + +
    +
  • + Sheet size: A4 +
  • +
  • + Maximum number of sheets: 2 +
  • +
  • + Print on both sides: no +
  • +
  • + Print colour: black +
  • +
  • + Envelope size: C5 +
  • +
  • + Dispatch time: standard +
  • +
  • + Postage: economy +
  • +
+
+
+
+ + + Go back + +
+
+
+
+
+
+
+`; + +exports[`valid template renders errors when no variant is selected and error state is returned 1`] = ` + +
+
+ +
+
+

+ Choose a printing and postage option +

+

+ We only show options that are available for this letter template. Select one option. +

+

+ + Learn about printing and postage (opens in a new tab). + +

+
+ + + + +
+ + + Error: + + Choose a printing and postage option + + + + + + + + + + + + + + + + + + + + + +
+ Select + + Name + + Details +
+ + +
+ + + + +
+
+ + + + + + +
    +
  • + Sheet size: A4 +
  • +
  • + Maximum number of sheets: 4 +
  • +
  • + Print on both sides: yes +
  • +
  • + Print colour: colour +
  • +
  • + Envelope size: C4 +
  • +
  • + Dispatch time: standard +
  • +
  • + Postage: first class +
  • +
+
+ + +
+ + + + +
+
+ + + + + + +
    +
  • + Sheet size: A4 +
  • +
  • + Maximum number of sheets: 2 +
  • +
  • + Print on both sides: no +
  • +
  • + Print colour: black +
  • +
  • + Envelope size: C5 +
  • +
  • + Dispatch time: standard +
  • +
  • + Postage: economy +
  • +
+
+
+
+ + + Go back + +
+
+
+
+
+
+
+`; 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 >