diff --git a/frontend/src/__tests__/app/choose-templates/page.test.tsx b/frontend/src/__tests__/app/choose-templates/page.test.tsx index d10699f00..93d314bac 100644 --- a/frontend/src/__tests__/app/choose-templates/page.test.tsx +++ b/frontend/src/__tests__/app/choose-templates/page.test.tsx @@ -772,7 +772,7 @@ describe('ChooseTemplatesPage', () => { ); await user.click(moveToProductionButton); - const errorSummary = screen.getByRole('alert'); + const errorSummary = await screen.findByRole('alert'); expect( within(errorSummary).getByTestId('error-summary') @@ -811,7 +811,7 @@ describe('ChooseTemplatesPage', () => { ); await user.click(moveToProductionButton); - const errorLinks = screen.getAllByRole('link', { + const errorLinks = await screen.findAllByRole('link', { name: /You have not chosen a template for your/, }); expect(errorLinks).toHaveLength(4); diff --git a/frontend/src/__tests__/app/upload-british-sign-language-letter-template/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/upload-british-sign-language-letter-template/__snapshots__/page.test.tsx.snap new file mode 100644 index 000000000..c6fa1ce4d --- /dev/null +++ b/frontend/src/__tests__/app/upload-british-sign-language-letter-template/__snapshots__/page.test.tsx.snap @@ -0,0 +1,1050 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`client has multiple campaign ids matches snapshot on initial render 1`] = ` + + + Back to choose a template type + +
+
+
+

+ Upload a British Sign Language letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ +
+
+ +
+ Choose which campaign this letter is for. +
+ +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ +
+ +
+
+
+

+ How to create a British Sign Language letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has multiple campaign ids renders errors when blank form is submitted and error state is returned 1`] = ` + + + Back to choose a template type + +
+ +
+
+

+ Upload a British Sign Language letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ + + Error: + + Enter a template name + + +
+
+ +
+ Choose which campaign this letter is for. +
+ + + Error: + + Choose a campaign + + +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ + + Error: + + Choose a template file + + +
+ +
+
+
+

+ How to create a British Sign Language letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has one campaign id matches snapshot on initial render 1`] = ` + + + Back to choose a template type + +
+
+
+

+ Upload a British Sign Language letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ +
+
+ +
+ This message plan will link to your only campaign: +
+ +

+ Campaign 1 +

+
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ +
+ +
+
+
+

+ How to create a British Sign Language letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has one campaign id renders errors when blank form is submitted and error state is returned 1`] = ` + + + Back to choose a template type + +
+ +
+
+

+ Upload a British Sign Language letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ + + Error: + + Enter a template name + + +
+
+ +
+ This message plan will link to your only campaign: +
+ +

+ Campaign 1 +

+
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ + + Error: + + Choose a template file + + +
+ +
+
+
+

+ How to create a British Sign Language letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; diff --git a/frontend/src/__tests__/app/upload-british-sign-language-letter-template/page.test.tsx b/frontend/src/__tests__/app/upload-british-sign-language-letter-template/page.test.tsx new file mode 100644 index 000000000..54a1f9b65 --- /dev/null +++ b/frontend/src/__tests__/app/upload-british-sign-language-letter-template/page.test.tsx @@ -0,0 +1,215 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { redirect, RedirectType } from 'next/navigation'; +import { verifyFormCsrfToken } from '@utils/csrf-utils'; +import { fetchClient } from '@utils/server-features'; +import Page, { + metadata, +} from '@app/upload-british-sign-language-letter-template/page'; +import { uploadBSLLetterTemplate } from '@app/upload-british-sign-language-letter-template/server-action'; + +jest.mock('next/navigation'); +jest.mock('@utils/csrf-utils'); +jest.mock('@utils/server-features'); +jest.mock('@app/upload-british-sign-language-letter-template/server-action'); + +beforeEach(() => { + jest.resetAllMocks(); + jest.mocked(verifyFormCsrfToken).mockResolvedValue(true); + jest.mocked(uploadBSLLetterTemplate).mockResolvedValue({}); +}); + +test('metadata', () => { + expect(metadata).toEqual({ + title: 'Upload a British Sign Language letter template - NHS Notify', + }); +}); + +describe('client has letter authoring feature flag disabled', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: [], + features: { + letterAuthoring: false, + }, + }); + }); + + it('redirects to campaign id required page', async () => { + await Page(); + + expect(redirect).toHaveBeenCalledWith( + '/choose-a-template-type', + RedirectType.replace + ); + }); +}); + +describe('client has no campaign ids', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: [], + features: { + letterAuthoring: true, + }, + }); + }); + + it('redirects to campaign id required page', async () => { + await Page(); + + expect(redirect).toHaveBeenCalledWith( + '/upload-letter-template/client-id-and-campaign-id-required', + RedirectType.replace + ); + }); +}); + +describe('client has one campaign id', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: ['Campaign 1'], + features: { + letterAuthoring: true, + }, + }); + }); + + it('matches snapshot on initial render', async () => { + expect(render(await Page()).asFragment()).toMatchSnapshot(); + }); + + it('submits the form with correct data', async () => { + const user = userEvent.setup(); + + render(await Page()); + + await user.click(screen.getByLabelText('Template name')); + await user.keyboard('A new template'); + + const file = new File(['hello'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + + await user.upload(screen.getByLabelText('Template file'), file); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadBSLLetterTemplate).toHaveBeenCalledTimes(1); + + const callArgs = jest.mocked(uploadBSLLetterTemplate).mock.calls[0]; + const formData = callArgs[1] as FormData; + + expect(formData.get('name')).toBe('A new template'); + expect(formData.get('campaignId')).toBe('Campaign 1'); + expect(formData.get('file')).toBeInstanceOf(File); + + expect( + screen.queryByRole('alert', { name: 'There is a problem' }) + ).not.toBeInTheDocument(); + }); + + it('renders errors when blank form is submitted and error state is returned', async () => { + jest.mocked(uploadBSLLetterTemplate).mockResolvedValue({ + errorState: { + fieldErrors: { + name: ['Enter a template name'], + file: ['Choose a template file'], + }, + }, + }); + + const user = userEvent.setup(); + + const page = render(await Page()); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect( + await screen.findByRole('alert', { name: 'There is a problem' }) + ).toBeInTheDocument(); + + expect(page.asFragment()).toMatchSnapshot(); + }); +}); + +describe('client has multiple campaign ids', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: ['Campaign 1', 'Campaign 2'], + features: { + letterAuthoring: true, + }, + }); + }); + + it('matches snapshot on initial render', async () => { + expect(render(await Page()).asFragment()).toMatchSnapshot(); + }); + + it('submits the form with correct data', async () => { + const user = userEvent.setup(); + + render(await Page()); + + await user.click(screen.getByLabelText('Template name')); + await user.keyboard('A new template'); + + await user.selectOptions(screen.getByLabelText('Campaign'), 'Campaign 2'); + + const file = new File(['hello'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + + await user.upload(screen.getByLabelText('Template file'), file); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadBSLLetterTemplate).toHaveBeenCalledTimes(1); + + const callArgs = jest.mocked(uploadBSLLetterTemplate).mock.calls[0]; + const formData = callArgs[1] as FormData; + + expect(formData.get('name')).toBe('A new template'); + expect(formData.get('campaignId')).toBe('Campaign 2'); + expect(formData.get('file')).toBeInstanceOf(File); + + expect( + screen.queryByRole('alert', { name: 'There is a problem' }) + ).not.toBeInTheDocument(); + }); + + it('renders errors when blank form is submitted and error state is returned', async () => { + jest.mocked(uploadBSLLetterTemplate).mockResolvedValue({ + errorState: { + fieldErrors: { + name: ['Enter a template name'], + campaignId: ['Choose a campaign'], + file: ['Choose a template file'], + }, + }, + }); + + const user = userEvent.setup(); + + const page = render(await Page()); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadBSLLetterTemplate).toHaveBeenCalledTimes(1); + + expect( + await screen.findByRole('alert', { name: 'There is a problem' }) + ).toBeInTheDocument(); + + expect(page.asFragment()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/__tests__/app/upload-british-sign-language-letter-template/server-action.test.ts b/frontend/src/__tests__/app/upload-british-sign-language-letter-template/server-action.test.ts new file mode 100644 index 000000000..11d3851fa --- /dev/null +++ b/frontend/src/__tests__/app/upload-british-sign-language-letter-template/server-action.test.ts @@ -0,0 +1,152 @@ +import { uploadBSLLetterTemplate } from '@app/upload-british-sign-language-letter-template/server-action'; + +describe('uploadBSLLetterTemplate', () => { + it('returns success when all fields are valid', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadBSLLetterTemplate({}, formData); + + expect(result.errorState).toBeUndefined(); + expect(result.fields).toEqual({ + name: 'Test Template', + campaignId: 'Campaign 1', + }); + }); + + it('returns validation error when name is empty', async () => { + const formData = new FormData(); + formData.append('name', ''); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadBSLLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + }, + }); + }); + + it('returns validation error when name is missing', async () => { + const formData = new FormData(); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadBSLLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + }, + }); + }); + + it('returns validation error when campaignId is empty', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', ''); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadBSLLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + campaignId: ['Choose a campaign'], + }, + }); + }); + + it('returns validation error when campaignId is missing', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadBSLLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + campaignId: ['Choose a campaign'], + }, + }); + }); + + it('returns validation error when file is missing', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const result = await uploadBSLLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + file: ['Choose a template file'], + }, + }); + }); + + it('returns validation error when file has incorrect MIME type', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.pdf', { + type: 'application/pdf', + }); + formData.append('file', file); + + const result = await uploadBSLLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + file: ['Choose a template file'], + }, + }); + }); + + it('returns multiple validation errors when multiple fields are invalid', async () => { + const formData = new FormData(); + formData.append('name', ''); + formData.append('campaignId', ''); + + const result = await uploadBSLLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + campaignId: ['Choose a campaign'], + file: ['Choose a template file'], + }, + }); + }); +}); diff --git a/frontend/src/__tests__/app/upload-large-print-letter-template/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/upload-large-print-letter-template/__snapshots__/page.test.tsx.snap new file mode 100644 index 000000000..b1c70a859 --- /dev/null +++ b/frontend/src/__tests__/app/upload-large-print-letter-template/__snapshots__/page.test.tsx.snap @@ -0,0 +1,1050 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`client has multiple campaign ids matches snapshot on initial render 1`] = ` + + + Back to choose a template type + +
+
+
+

+ Upload a large print letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ +
+
+ +
+ Choose which campaign this letter is for. +
+ +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ +
+ +
+
+
+

+ How to create a large print letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has multiple campaign ids renders errors when blank form is submitted and error state is returned 1`] = ` + + + Back to choose a template type + +
+ +
+
+

+ Upload a large print letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ + + Error: + + Enter a template name + + +
+
+ +
+ Choose which campaign this letter is for. +
+ + + Error: + + Choose a campaign + + +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ + + Error: + + Choose a template file + + +
+ +
+
+
+

+ How to create a large print letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has one campaign id matches snapshot on initial render 1`] = ` + + + Back to choose a template type + +
+
+
+

+ Upload a large print letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ +
+
+ +
+ This message plan will link to your only campaign: +
+ +

+ Campaign 1 +

+
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ +
+ +
+
+
+

+ How to create a large print letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has one campaign id renders errors when blank form is submitted and error state is returned 1`] = ` + + + Back to choose a template type + +
+ +
+
+

+ Upload a large print letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ + + Error: + + Enter a template name + + +
+
+ +
+ This message plan will link to your only campaign: +
+ +

+ Campaign 1 +

+
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ + + Error: + + Choose a template file + + +
+ +
+
+
+

+ How to create a large print letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; diff --git a/frontend/src/__tests__/app/upload-large-print-letter-template/page.test.tsx b/frontend/src/__tests__/app/upload-large-print-letter-template/page.test.tsx new file mode 100644 index 000000000..bf53e42c3 --- /dev/null +++ b/frontend/src/__tests__/app/upload-large-print-letter-template/page.test.tsx @@ -0,0 +1,213 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { redirect, RedirectType } from 'next/navigation'; +import { verifyFormCsrfToken } from '@utils/csrf-utils'; +import { fetchClient } from '@utils/server-features'; +import Page, { metadata } from '@app/upload-large-print-letter-template/page'; +import { uploadLargePrintLetterTemplate } from '@app/upload-large-print-letter-template/server-action'; + +jest.mock('next/navigation'); +jest.mock('@utils/csrf-utils'); +jest.mock('@utils/server-features'); +jest.mock('@app/upload-large-print-letter-template/server-action'); + +beforeEach(() => { + jest.resetAllMocks(); + jest.mocked(verifyFormCsrfToken).mockResolvedValue(true); + jest.mocked(uploadLargePrintLetterTemplate).mockResolvedValue({}); +}); + +test('metadata', () => { + expect(metadata).toEqual({ + title: 'Upload a large print letter template - NHS Notify', + }); +}); + +describe('client has letter authoring feature flag disabled', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: [], + features: { + letterAuthoring: false, + }, + }); + }); + + it('redirects to campaign id required page', async () => { + await Page(); + + expect(redirect).toHaveBeenCalledWith( + '/choose-a-template-type', + RedirectType.replace + ); + }); +}); + +describe('client has no campaign ids', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: [], + features: { + letterAuthoring: true, + }, + }); + }); + + it('redirects to campaign id required page', async () => { + await Page(); + + expect(redirect).toHaveBeenCalledWith( + '/upload-letter-template/client-id-and-campaign-id-required', + RedirectType.replace + ); + }); +}); + +describe('client has one campaign id', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: ['Campaign 1'], + features: { + letterAuthoring: true, + }, + }); + }); + + it('matches snapshot on initial render', async () => { + expect(render(await Page()).asFragment()).toMatchSnapshot(); + }); + + it('submits the form with correct data', async () => { + const user = userEvent.setup(); + + render(await Page()); + + await user.click(screen.getByLabelText('Template name')); + await user.keyboard('A new template'); + + const file = new File(['hello'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + + await user.upload(screen.getByLabelText('Template file'), file); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadLargePrintLetterTemplate).toHaveBeenCalledTimes(1); + + const callArgs = jest.mocked(uploadLargePrintLetterTemplate).mock.calls[0]; + const formData = callArgs[1] as FormData; + + expect(formData.get('name')).toBe('A new template'); + expect(formData.get('campaignId')).toBe('Campaign 1'); + expect(formData.get('file')).toBeInstanceOf(File); + + expect( + screen.queryByRole('alert', { name: 'There is a problem' }) + ).not.toBeInTheDocument(); + }); + + it('renders errors when blank form is submitted and error state is returned', async () => { + jest.mocked(uploadLargePrintLetterTemplate).mockResolvedValue({ + errorState: { + fieldErrors: { + name: ['Enter a template name'], + file: ['Choose a template file'], + }, + }, + }); + + const user = userEvent.setup(); + + const page = render(await Page()); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect( + await screen.findByRole('alert', { name: 'There is a problem' }) + ).toBeInTheDocument(); + + expect(page.asFragment()).toMatchSnapshot(); + }); +}); + +describe('client has multiple campaign ids', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: ['Campaign 1', 'Campaign 2'], + features: { + letterAuthoring: true, + }, + }); + }); + + it('matches snapshot on initial render', async () => { + expect(render(await Page()).asFragment()).toMatchSnapshot(); + }); + + it('submits the form with correct data', async () => { + const user = userEvent.setup(); + + render(await Page()); + + await user.click(screen.getByLabelText('Template name')); + await user.keyboard('A new template'); + + await user.selectOptions(screen.getByLabelText('Campaign'), 'Campaign 2'); + + const file = new File(['hello'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + + await user.upload(screen.getByLabelText('Template file'), file); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadLargePrintLetterTemplate).toHaveBeenCalledTimes(1); + + const callArgs = jest.mocked(uploadLargePrintLetterTemplate).mock.calls[0]; + const formData = callArgs[1] as FormData; + + expect(formData.get('name')).toBe('A new template'); + expect(formData.get('campaignId')).toBe('Campaign 2'); + expect(formData.get('file')).toBeInstanceOf(File); + + expect( + screen.queryByRole('alert', { name: 'There is a problem' }) + ).not.toBeInTheDocument(); + }); + + it('renders errors when blank form is submitted and error state is returned', async () => { + jest.mocked(uploadLargePrintLetterTemplate).mockResolvedValue({ + errorState: { + fieldErrors: { + name: ['Enter a template name'], + campaignId: ['Choose a campaign'], + file: ['Choose a template file'], + }, + }, + }); + + const user = userEvent.setup(); + + const page = render(await Page()); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadLargePrintLetterTemplate).toHaveBeenCalledTimes(1); + + expect( + await screen.findByRole('alert', { name: 'There is a problem' }) + ).toBeInTheDocument(); + + expect(page.asFragment()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/__tests__/app/upload-large-print-letter-template/server-action.test.ts b/frontend/src/__tests__/app/upload-large-print-letter-template/server-action.test.ts new file mode 100644 index 000000000..4927774c1 --- /dev/null +++ b/frontend/src/__tests__/app/upload-large-print-letter-template/server-action.test.ts @@ -0,0 +1,152 @@ +import { uploadLargePrintLetterTemplate } from '@app/upload-large-print-letter-template/server-action'; + +describe('uploadLargePrintLetterTemplate', () => { + it('returns success when all fields are valid', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadLargePrintLetterTemplate({}, formData); + + expect(result.errorState).toBeUndefined(); + expect(result.fields).toEqual({ + name: 'Test Template', + campaignId: 'Campaign 1', + }); + }); + + it('returns validation error when name is empty', async () => { + const formData = new FormData(); + formData.append('name', ''); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadLargePrintLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + }, + }); + }); + + it('returns validation error when name is missing', async () => { + const formData = new FormData(); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadLargePrintLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + }, + }); + }); + + it('returns validation error when campaignId is empty', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', ''); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadLargePrintLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + campaignId: ['Choose a campaign'], + }, + }); + }); + + it('returns validation error when campaignId is missing', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadLargePrintLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + campaignId: ['Choose a campaign'], + }, + }); + }); + + it('returns validation error when file is missing', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const result = await uploadLargePrintLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + file: ['Choose a template file'], + }, + }); + }); + + it('returns validation error when file has incorrect MIME type', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.pdf', { + type: 'application/pdf', + }); + formData.append('file', file); + + const result = await uploadLargePrintLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + file: ['Choose a template file'], + }, + }); + }); + + it('returns multiple validation errors when multiple fields are invalid', async () => { + const formData = new FormData(); + formData.append('name', ''); + formData.append('campaignId', ''); + + const result = await uploadLargePrintLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + campaignId: ['Choose a campaign'], + file: ['Choose a template file'], + }, + }); + }); +}); diff --git a/frontend/src/__tests__/app/upload-other-language-letter-template/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/upload-other-language-letter-template/__snapshots__/page.test.tsx.snap new file mode 100644 index 000000000..58b409b2c --- /dev/null +++ b/frontend/src/__tests__/app/upload-other-language-letter-template/__snapshots__/page.test.tsx.snap @@ -0,0 +1,2158 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`client has multiple campaign ids matches snapshot on initial render 1`] = ` + + + Back to choose a template type + +
+
+
+

+ Upload an other language letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ +
+
+ +
+ Choose which campaign this letter is for. +
+ +
+
+ +
+ Choose the language used in this template. +
+ +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ +
+ +
+
+
+

+ How to create an other language letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has multiple campaign ids renders errors when blank form is submitted and error state is returned 1`] = ` + + + Back to choose a template type + +
+ +
+
+

+ Upload an other language letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ + + Error: + + Enter a template name + + +
+
+ +
+ Choose which campaign this letter is for. +
+ + + Error: + + Choose a campaign + + +
+
+ +
+ Choose the language used in this template. +
+ + + Error: + + Choose a language + + +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ + + Error: + + Choose a template file + + +
+ +
+
+
+

+ How to create an other language letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has one campaign id matches snapshot on initial render 1`] = ` + + + Back to choose a template type + +
+
+
+

+ Upload an other language letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ +
+
+ +
+ This message plan will link to your only campaign: +
+ +

+ Campaign 1 +

+
+
+ +
+ Choose the language used in this template. +
+ +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ +
+ +
+
+
+

+ How to create an other language letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has one campaign id renders errors when blank form is submitted and error state is returned 1`] = ` + + + Back to choose a template type + +
+ +
+
+

+ Upload an other language letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ + + Error: + + Enter a template name + + +
+
+ +
+ This message plan will link to your only campaign: +
+ +

+ Campaign 1 +

+
+
+ +
+ Choose the language used in this template. +
+ + + Error: + + Choose a language + + +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ + + Error: + + Choose a template file + + +
+ +
+
+
+

+ How to create an other language letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has one campaign id renders the rtl template link if an rtl language is selected 1`] = ` + + + Back to choose a template type + +
+
+
+

+ Upload an other language letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ +
+
+ +
+ This message plan will link to your only campaign: +
+ +

+ Campaign 1 +

+
+
+ +
+ Choose the language used in this template. +
+ +
+
+ + Information: + +

+ + Right-to-left language selected + +

+

+ You've selected a language that reads right-to-left. Make sure you use the + + other language (right-aligned) letter template file (opens in a new tab) + + . +

+
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ +
+ +
+
+
+

+ How to create an other language letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; diff --git a/frontend/src/__tests__/app/upload-other-language-letter-template/page.test.tsx b/frontend/src/__tests__/app/upload-other-language-letter-template/page.test.tsx new file mode 100644 index 000000000..76ed94d25 --- /dev/null +++ b/frontend/src/__tests__/app/upload-other-language-letter-template/page.test.tsx @@ -0,0 +1,280 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { redirect, RedirectType } from 'next/navigation'; +import { verifyFormCsrfToken } from '@utils/csrf-utils'; +import { fetchClient } from '@utils/server-features'; +import Page, { + metadata, +} from '@app/upload-other-language-letter-template/page'; +import { uploadOtherLanguageLetterTemplate } from '@app/upload-other-language-letter-template/server-action'; + +jest.mock('next/navigation'); +jest.mock('@utils/csrf-utils'); +jest.mock('@utils/server-features'); +jest.mock('@app/upload-other-language-letter-template/server-action'); + +beforeEach(() => { + jest.resetAllMocks(); + jest.mocked(verifyFormCsrfToken).mockResolvedValue(true); + jest.mocked(uploadOtherLanguageLetterTemplate).mockResolvedValue({}); +}); + +test('metadata', () => { + expect(metadata).toEqual({ + title: 'Upload an other language letter template - NHS Notify', + }); +}); + +describe('client has letter authoring feature flag disabled', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: [], + features: { + letterAuthoring: false, + }, + }); + }); + + it('redirects to campaign id required page', async () => { + await Page(); + + expect(redirect).toHaveBeenCalledWith( + '/choose-a-template-type', + RedirectType.replace + ); + }); +}); + +describe('client has no campaign ids', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: [], + features: { + letterAuthoring: true, + }, + }); + }); + + it('redirects to campaign id required page', async () => { + await Page(); + + expect(redirect).toHaveBeenCalledWith( + '/upload-letter-template/client-id-and-campaign-id-required', + RedirectType.replace + ); + }); +}); + +describe('client has one campaign id', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: ['Campaign 1'], + features: { + letterAuthoring: true, + }, + }); + }); + + it('matches snapshot on initial render', async () => { + expect(render(await Page()).asFragment()).toMatchSnapshot(); + }); + + it('submits the form with correct data', async () => { + const user = userEvent.setup(); + + render(await Page()); + + await user.click(screen.getByLabelText('Template name')); + await user.keyboard('A new template'); + + await user.selectOptions( + screen.getByLabelText('Template language'), + 'Slovak' + ); + + const file = new File(['hello'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + + await user.upload(screen.getByLabelText('Template file'), file); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadOtherLanguageLetterTemplate).toHaveBeenCalledTimes(1); + + const callArgs = jest.mocked(uploadOtherLanguageLetterTemplate).mock + .calls[0]; + const formData = callArgs[1] as FormData; + + expect(formData.get('name')).toBe('A new template'); + expect(formData.get('campaignId')).toBe('Campaign 1'); + expect(formData.get('file')).toBeInstanceOf(File); + expect(formData.get('language')).toBe('sk'); + + expect( + screen.queryByRole('alert', { name: 'There is a problem' }) + ).not.toBeInTheDocument(); + }); + + it('renders errors when blank form is submitted and error state is returned', async () => { + jest.mocked(uploadOtherLanguageLetterTemplate).mockResolvedValue({ + errorState: { + fieldErrors: { + name: ['Enter a template name'], + file: ['Choose a template file'], + language: ['Choose a language'], + }, + }, + }); + + const user = userEvent.setup(); + + const page = render(await Page()); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect( + await screen.findByRole('alert', { name: 'There is a problem' }) + ).toBeInTheDocument(); + + expect(page.asFragment()).toMatchSnapshot(); + }); + + it('renders the rtl template link if an rtl language is selected', async () => { + const user = userEvent.setup(); + + const page = render(await Page()); + + const rtlLinkName = /other language \(right-aligned\) letter template file/; + + expect( + page.queryByRole('link', { + name: rtlLinkName, + }) + ).not.toBeInTheDocument(); + + await user.selectOptions( + screen.getByLabelText('Template language'), + 'Slovak' + ); + + expect( + page.queryByRole('link', { + name: rtlLinkName, + }) + ).not.toBeInTheDocument(); + + await user.selectOptions( + screen.getByLabelText('Template language'), + 'Arabic' + ); + + expect( + await page.findByRole('link', { + name: rtlLinkName, + }) + ).toBeInTheDocument(); + + expect(page.asFragment()).toMatchSnapshot(); + + await user.selectOptions( + screen.getByLabelText('Template language'), + 'Slovak' + ); + + expect( + page.queryByRole('link', { + name: rtlLinkName, + }) + ).not.toBeInTheDocument(); + }); +}); + +describe('client has multiple campaign ids', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: ['Campaign 1', 'Campaign 2'], + features: { + letterAuthoring: true, + }, + }); + }); + + it('matches snapshot on initial render', async () => { + expect(render(await Page()).asFragment()).toMatchSnapshot(); + }); + + it('submits the form with correct data', async () => { + const user = userEvent.setup(); + + render(await Page()); + + await user.click(screen.getByLabelText('Template name')); + await user.keyboard('A new template'); + + await user.selectOptions(screen.getByLabelText('Campaign'), 'Campaign 2'); + + await user.selectOptions( + screen.getByLabelText('Template language'), + 'Slovak' + ); + + const file = new File(['hello'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + + await user.upload(screen.getByLabelText('Template file'), file); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadOtherLanguageLetterTemplate).toHaveBeenCalledTimes(1); + + const callArgs = jest.mocked(uploadOtherLanguageLetterTemplate).mock + .calls[0]; + const formData = callArgs[1] as FormData; + + expect(formData.get('name')).toBe('A new template'); + expect(formData.get('campaignId')).toBe('Campaign 2'); + expect(formData.get('file')).toBeInstanceOf(File); + expect(formData.get('language')).toBe('sk'); + + expect( + screen.queryByRole('alert', { name: 'There is a problem' }) + ).not.toBeInTheDocument(); + }); + + it('renders errors when blank form is submitted and error state is returned', async () => { + jest.mocked(uploadOtherLanguageLetterTemplate).mockResolvedValue({ + errorState: { + fieldErrors: { + name: ['Enter a template name'], + campaignId: ['Choose a campaign'], + language: ['Choose a language'], + file: ['Choose a template file'], + }, + }, + }); + + const user = userEvent.setup(); + + const page = render(await Page()); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadOtherLanguageLetterTemplate).toHaveBeenCalledTimes(1); + + expect( + await screen.findByRole('alert', { name: 'There is a problem' }) + ).toBeInTheDocument(); + + expect(page.asFragment()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/__tests__/app/upload-other-language-letter-template/server-action.test.ts b/frontend/src/__tests__/app/upload-other-language-letter-template/server-action.test.ts new file mode 100644 index 000000000..2d0ca6cb1 --- /dev/null +++ b/frontend/src/__tests__/app/upload-other-language-letter-template/server-action.test.ts @@ -0,0 +1,224 @@ +import { uploadOtherLanguageLetterTemplate } from '@app/upload-other-language-letter-template/server-action'; + +describe('uploadOtherLanguageLetterTemplate', () => { + it('returns success when all fields are valid', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + formData.append('language', 'lv'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toBeUndefined(); + expect(result.fields).toEqual({ + name: 'Test Template', + campaignId: 'Campaign 1', + language: 'lv', + }); + }); + + it('returns validation error when name is empty', async () => { + const formData = new FormData(); + formData.append('name', ''); + formData.append('campaignId', 'Campaign 1'); + formData.append('language', 'lv'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + }, + }); + }); + + it('returns validation error when name is missing', async () => { + const formData = new FormData(); + formData.append('campaignId', 'Campaign 1'); + formData.append('language', 'lv'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + }, + }); + }); + + it('returns validation error when campaignId is empty', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', ''); + formData.append('language', 'lv'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + campaignId: ['Choose a campaign'], + }, + }); + }); + + it('returns validation error when campaignId is missing', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('language', 'lv'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + campaignId: ['Choose a campaign'], + }, + }); + }); + + it('returns validation error when language is empty', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + formData.append('language', ''); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + language: ['Choose a language'], + }, + }); + }); + + it('returns validation error when language is missing', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + language: ['Choose a language'], + }, + }); + }); + + it('returns validation error when language is invalid', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + formData.append('language', 'not a language code'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + language: ['Choose a language'], + }, + }); + }); + + it('returns validation error when file is missing', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + formData.append('language', 'lv'); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + file: ['Choose a template file'], + }, + }); + }); + + it('returns validation error when file has incorrect MIME type', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + formData.append('language', 'lv'); + + const file = new File(['content'], 'template.pdf', { + type: 'application/pdf', + }); + formData.append('file', file); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + file: ['Choose a template file'], + }, + }); + }); + + it('returns multiple validation errors when multiple fields are invalid', async () => { + const formData = new FormData(); + formData.append('name', ''); + formData.append('campaignId', ''); + formData.append('language', ''); + + const result = await uploadOtherLanguageLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + campaignId: ['Choose a campaign'], + language: ['Choose a language'], + file: ['Choose a template file'], + }, + }); + }); +}); diff --git a/frontend/src/__tests__/app/upload-standard-english-letter-template/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/upload-standard-english-letter-template/__snapshots__/page.test.tsx.snap new file mode 100644 index 000000000..cf5a56722 --- /dev/null +++ b/frontend/src/__tests__/app/upload-standard-english-letter-template/__snapshots__/page.test.tsx.snap @@ -0,0 +1,1050 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`client has multiple campaign ids matches snapshot on initial render 1`] = ` + + + Back to choose a template type + +
+
+
+

+ Upload a standard English letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ +
+
+ +
+ Choose which campaign this letter is for. +
+ +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ +
+ +
+
+
+

+ How to create a standard English letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has multiple campaign ids renders errors when blank form is submitted and error state is returned 1`] = ` + + + Back to choose a template type + +
+ +
+
+

+ Upload a standard English letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ + + Error: + + Enter a template name + + +
+
+ +
+ Choose which campaign this letter is for. +
+ + + Error: + + Choose a campaign + + +
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ + + Error: + + Choose a template file + + +
+ +
+
+
+

+ How to create a standard English letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has one campaign id matches snapshot on initial render 1`] = ` + + + Back to choose a template type + +
+
+
+

+ Upload a standard English letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ +
+
+ +
+ This message plan will link to your only campaign: +
+ +

+ Campaign 1 +

+
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ +
+ +
+
+
+

+ How to create a standard English letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; + +exports[`client has one campaign id renders errors when blank form is submitted and error state is returned 1`] = ` + + + Back to choose a template type + +
+ +
+
+

+ Upload a standard English letter template +

+
+
+
+
+
+ + +
+ +
+ This will not be visible to recipients. +
+
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+ + + Error: + + Enter a template name + + +
+
+ +
+ This message plan will link to your only campaign: +
+ +

+ Campaign 1 +

+
+
+ +
+ Only upload your final letter template file. +
+ Make sure you use one of our blank template files to create the letter. +
+ + + Error: + + Choose a template file + + +
+ +
+
+
+

+ How to create a standard English letter template +

+
    +
  1. + Download the relevant + + blank letter template file (opens in a new tab) + + . +
  2. +
  3. + Add + + formatting (opens in a new tab) + + . +
  4. +
  5. + Add any + + personalisation (opens in a new tab) + + . +
  6. +
  7. + Save your Microsoft Word file and upload it to this page. +
  8. +
+
+
+
+
+`; diff --git a/frontend/src/__tests__/app/upload-standard-english-letter-template/page.test.tsx b/frontend/src/__tests__/app/upload-standard-english-letter-template/page.test.tsx new file mode 100644 index 000000000..d3d2772a3 --- /dev/null +++ b/frontend/src/__tests__/app/upload-standard-english-letter-template/page.test.tsx @@ -0,0 +1,215 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { redirect, RedirectType } from 'next/navigation'; +import { verifyFormCsrfToken } from '@utils/csrf-utils'; +import { fetchClient } from '@utils/server-features'; +import Page, { + metadata, +} from '@app/upload-standard-english-letter-template/page'; +import { uploadStandardLetterTemplate } from '@app/upload-standard-english-letter-template/server-action'; + +jest.mock('next/navigation'); +jest.mock('@utils/csrf-utils'); +jest.mock('@utils/server-features'); +jest.mock('@app/upload-standard-english-letter-template/server-action'); + +beforeEach(() => { + jest.resetAllMocks(); + jest.mocked(verifyFormCsrfToken).mockResolvedValue(true); + jest.mocked(uploadStandardLetterTemplate).mockResolvedValue({}); +}); + +test('metadata', () => { + expect(metadata).toEqual({ + title: 'Upload a standard English letter template - NHS Notify', + }); +}); + +describe('client has letter authoring feature flag disabled', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: [], + features: { + letterAuthoring: false, + }, + }); + }); + + it('redirects to campaign id required page', async () => { + await Page(); + + expect(redirect).toHaveBeenCalledWith( + '/choose-a-template-type', + RedirectType.replace + ); + }); +}); + +describe('client has no campaign ids', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: [], + features: { + letterAuthoring: true, + }, + }); + }); + + it('redirects to campaign id required page', async () => { + await Page(); + + expect(redirect).toHaveBeenCalledWith( + '/upload-letter-template/client-id-and-campaign-id-required', + RedirectType.replace + ); + }); +}); + +describe('client has one campaign id', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: ['Campaign 1'], + features: { + letterAuthoring: true, + }, + }); + }); + + it('matches snapshot on initial render', async () => { + expect(render(await Page()).asFragment()).toMatchSnapshot(); + }); + + it('submits the form with correct data', async () => { + const user = userEvent.setup(); + + render(await Page()); + + await user.click(screen.getByLabelText('Template name')); + await user.keyboard('A new template'); + + const file = new File(['hello'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + + await user.upload(screen.getByLabelText('Template file'), file); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadStandardLetterTemplate).toHaveBeenCalledTimes(1); + + const callArgs = jest.mocked(uploadStandardLetterTemplate).mock.calls[0]; + const formData = callArgs[1] as FormData; + + expect(formData.get('name')).toBe('A new template'); + expect(formData.get('campaignId')).toBe('Campaign 1'); + expect(formData.get('file')).toBeInstanceOf(File); + + expect( + screen.queryByRole('alert', { name: 'There is a problem' }) + ).not.toBeInTheDocument(); + }); + + it('renders errors when blank form is submitted and error state is returned', async () => { + jest.mocked(uploadStandardLetterTemplate).mockResolvedValue({ + errorState: { + fieldErrors: { + name: ['Enter a template name'], + file: ['Choose a template file'], + }, + }, + }); + + const user = userEvent.setup(); + + const page = render(await Page()); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect( + await screen.findByRole('alert', { name: 'There is a problem' }) + ).toBeInTheDocument(); + + expect(page.asFragment()).toMatchSnapshot(); + }); +}); + +describe('client has multiple campaign ids', () => { + beforeEach(() => { + jest.mocked(fetchClient).mockResolvedValue({ + campaignIds: ['Campaign 1', 'Campaign 2'], + features: { + letterAuthoring: true, + }, + }); + }); + + it('matches snapshot on initial render', async () => { + expect(render(await Page()).asFragment()).toMatchSnapshot(); + }); + + it('submits the form with correct data', async () => { + const user = userEvent.setup(); + + render(await Page()); + + await user.click(screen.getByLabelText('Template name')); + await user.keyboard('A new template'); + + await user.selectOptions(screen.getByLabelText('Campaign'), 'Campaign 2'); + + const file = new File(['hello'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + + await user.upload(screen.getByLabelText('Template file'), file); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadStandardLetterTemplate).toHaveBeenCalledTimes(1); + + const callArgs = jest.mocked(uploadStandardLetterTemplate).mock.calls[0]; + const formData = callArgs[1] as FormData; + + expect(formData.get('name')).toBe('A new template'); + expect(formData.get('campaignId')).toBe('Campaign 2'); + expect(formData.get('file')).toBeInstanceOf(File); + + expect( + screen.queryByRole('alert', { name: 'There is a problem' }) + ).not.toBeInTheDocument(); + }); + + it('renders errors when blank form is submitted and error state is returned', async () => { + jest.mocked(uploadStandardLetterTemplate).mockResolvedValue({ + errorState: { + fieldErrors: { + name: ['Enter a template name'], + campaignId: ['Choose a campaign'], + file: ['Choose a template file'], + }, + }, + }); + + const user = userEvent.setup(); + + const page = render(await Page()); + + await user.click( + screen.getByRole('button', { name: 'Upload letter template file' }) + ); + + expect(uploadStandardLetterTemplate).toHaveBeenCalledTimes(1); + + expect( + await screen.findByRole('alert', { name: 'There is a problem' }) + ).toBeInTheDocument(); + + expect(page.asFragment()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/__tests__/app/upload-standard-english-letter-template/server-action.test.ts b/frontend/src/__tests__/app/upload-standard-english-letter-template/server-action.test.ts new file mode 100644 index 000000000..fb2418dda --- /dev/null +++ b/frontend/src/__tests__/app/upload-standard-english-letter-template/server-action.test.ts @@ -0,0 +1,152 @@ +import { uploadStandardLetterTemplate } from '@app/upload-standard-english-letter-template/server-action'; + +describe('uploadStandardLetterTemplate', () => { + it('returns success when all fields are valid', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadStandardLetterTemplate({}, formData); + + expect(result.errorState).toBeUndefined(); + expect(result.fields).toEqual({ + name: 'Test Template', + campaignId: 'Campaign 1', + }); + }); + + it('returns validation error when name is empty', async () => { + const formData = new FormData(); + formData.append('name', ''); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadStandardLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + }, + }); + }); + + it('returns validation error when name is missing', async () => { + const formData = new FormData(); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadStandardLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + }, + }); + }); + + it('returns validation error when campaignId is empty', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', ''); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadStandardLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + campaignId: ['Choose a campaign'], + }, + }); + }); + + it('returns validation error when campaignId is missing', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + + const file = new File(['content'], 'template.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + formData.append('file', file); + + const result = await uploadStandardLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + campaignId: ['Choose a campaign'], + }, + }); + }); + + it('returns validation error when file is missing', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const result = await uploadStandardLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + file: ['Choose a template file'], + }, + }); + }); + + it('returns validation error when file has incorrect MIME type', async () => { + const formData = new FormData(); + formData.append('name', 'Test Template'); + formData.append('campaignId', 'Campaign 1'); + + const file = new File(['content'], 'template.pdf', { + type: 'application/pdf', + }); + formData.append('file', file); + + const result = await uploadStandardLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + file: ['Choose a template file'], + }, + }); + }); + + it('returns multiple validation errors when multiple fields are invalid', async () => { + const formData = new FormData(); + formData.append('name', ''); + formData.append('campaignId', ''); + + const result = await uploadStandardLetterTemplate({}, formData); + + expect(result.errorState).toEqual({ + formErrors: [], + fieldErrors: { + name: ['Enter a template name'], + campaignId: ['Choose a campaign'], + file: ['Choose a template file'], + }, + }); + }); +}); diff --git a/frontend/src/__tests__/components/forms/EmailTemplateForm/__snapshots__/EmailTemplateForm.test.tsx.snap b/frontend/src/__tests__/components/forms/EmailTemplateForm/__snapshots__/EmailTemplateForm.test.tsx.snap index edd320313..d1c3709f1 100644 --- a/frontend/src/__tests__/components/forms/EmailTemplateForm/__snapshots__/EmailTemplateForm.test.tsx.snap +++ b/frontend/src/__tests__/components/forms/EmailTemplateForm/__snapshots__/EmailTemplateForm.test.tsx.snap @@ -79,11 +79,9 @@ exports[`Client-side validation triggers - invalid form - errors displayed 1`] =

You should name your templates in a way that works best for your service or organisation. @@ -115,9 +112,7 @@ exports[`Client-side validation triggers - invalid form - errors displayed 1`] = version number of the template -

+

For example, 'Email - covid19 2023 - over 65s - version 3'

@@ -783,11 +778,9 @@ exports[`Client-side validation triggers - valid form - no errors 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -819,9 +811,7 @@ exports[`Client-side validation triggers - valid form - no errors 1`] = ` version number of the template -

+

For example, 'Email - covid19 2023 - over 65s - version 3'

@@ -1507,11 +1497,9 @@ exports[`renders page one error 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -1543,9 +1530,7 @@ exports[`renders page one error 1`] = ` version number of the template -

+

For example, 'Email - covid19 2023 - over 65s - version 3'

@@ -2310,11 +2295,9 @@ exports[`renders page with multiple errors 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -2346,9 +2328,7 @@ exports[`renders page with multiple errors 1`] = ` version number of the template -

+

For example, 'Email - covid19 2023 - over 65s - version 3'

@@ -3092,11 +3072,9 @@ exports[`renders page with preloaded field values 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -3128,9 +3105,7 @@ exports[`renders page with preloaded field values 1`] = ` version number of the template -

+

For example, 'Email - covid19 2023 - over 65s - version 3'

@@ -3784,11 +3759,9 @@ exports[`renders page without back link for initial state with id - edit mode 1`

You should name your templates in a way that works best for your service or organisation. @@ -3820,9 +3792,7 @@ exports[`renders page without back link for initial state with id - edit mode 1` version number of the template -

+

For example, 'Email - covid19 2023 - over 65s - version 3'

diff --git a/frontend/src/__tests__/components/forms/LetterTemplateForm/__snapshots__/LetterTemplateForm.test.tsx.snap b/frontend/src/__tests__/components/forms/LetterTemplateForm/__snapshots__/LetterTemplateForm.test.tsx.snap index fc91b2915..81b84ff52 100644 --- a/frontend/src/__tests__/components/forms/LetterTemplateForm/__snapshots__/LetterTemplateForm.test.tsx.snap +++ b/frontend/src/__tests__/components/forms/LetterTemplateForm/__snapshots__/LetterTemplateForm.test.tsx.snap @@ -89,11 +89,9 @@ exports[`Client-side validation triggers - valid form - with errors 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -125,9 +122,7 @@ exports[`Client-side validation triggers - valid form - with errors 1`] = ` version number of the template -

+

For example, 'Letter - covid19 2023 - over 65s - version 3'

@@ -544,11 +539,9 @@ exports[`Client-side validation triggers - valid form - without errors 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -580,9 +572,7 @@ exports[`Client-side validation triggers - valid form - without errors 1`] = ` version number of the template -

+

For example, 'Letter - covid19 2023 - over 65s - version 3'

@@ -977,11 +967,9 @@ exports[`hides right-to-left language warning when language changes 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -1013,9 +1000,7 @@ exports[`hides right-to-left language warning when language changes 1`] = ` version number of the template -

+

For example, 'Letter - covid19 2023 - over 65s - version 3'

@@ -1438,11 +1423,9 @@ exports[`renders page one error 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -1474,9 +1456,7 @@ exports[`renders page one error 1`] = ` version number of the template -

+

For example, 'Letter - covid19 2023 - over 65s - version 3'

@@ -1886,11 +1866,9 @@ exports[`renders page with multiple campaign ids 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -1922,9 +1899,7 @@ exports[`renders page with multiple campaign ids 1`] = ` version number of the template -

+

For example, 'Letter - covid19 2023 - over 65s - version 3'

@@ -2417,11 +2392,9 @@ exports[`renders page with multiple errors 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -2453,9 +2425,7 @@ exports[`renders page with multiple errors 1`] = ` version number of the template -

+

For example, 'Letter - covid19 2023 - over 65s - version 3'

@@ -2883,11 +2853,9 @@ exports[`renders page with preloaded field values 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -2919,9 +2886,7 @@ exports[`renders page with preloaded field values 1`] = ` version number of the template -

+

For example, 'Letter - covid19 2023 - over 65s - version 3'

@@ -3345,11 +3310,9 @@ exports[`shows right-to-left language warning when language changes 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -3381,9 +3343,7 @@ exports[`shows right-to-left language warning when language changes 1`] = ` version number of the template -

+

For example, 'Letter - covid19 2023 - over 65s - version 3'

diff --git a/frontend/src/__tests__/components/forms/NhsAppTemplateForm/__snapshots__/NhsAppTemplateForm.test.tsx.snap b/frontend/src/__tests__/components/forms/NhsAppTemplateForm/__snapshots__/NhsAppTemplateForm.test.tsx.snap index f87aaeb17..ebed15eea 100644 --- a/frontend/src/__tests__/components/forms/NhsAppTemplateForm/__snapshots__/NhsAppTemplateForm.test.tsx.snap +++ b/frontend/src/__tests__/components/forms/NhsAppTemplateForm/__snapshots__/NhsAppTemplateForm.test.tsx.snap @@ -86,11 +86,9 @@ exports[`Client-side validation triggers - invalid form - errors displayed 1`] =

You should name your templates in a way that works best for your service or organisation. @@ -122,9 +119,7 @@ exports[`Client-side validation triggers - invalid form - errors displayed 1`] = version number of the template -

+

For example, 'NHS App - covid19 2023 - over 65s - version 3'

@@ -182,7 +177,7 @@ exports[`Client-side validation triggers - invalid form - errors displayed 1`] = class="" >

16 of 5000 characters

@@ -780,11 +775,9 @@ exports[`Client-side validation triggers - valid form - no errors 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -816,9 +808,7 @@ exports[`Client-side validation triggers - valid form - no errors 1`] = ` version number of the template -

+

For example, 'NHS App - covid19 2023 - over 65s - version 3'

@@ -864,7 +854,7 @@ exports[`Client-side validation triggers - valid form - no errors 1`] = ` class="" >

16 of 5000 characters

@@ -1462,11 +1452,9 @@ exports[`renders page 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -1498,9 +1485,7 @@ exports[`renders page 1`] = ` version number of the template -

+

For example, 'NHS App - covid19 2023 - over 65s - version 3'

@@ -1546,7 +1531,7 @@ exports[`renders page 1`] = ` class="" >

16 of 5000 characters

@@ -2169,11 +2154,9 @@ exports[`renders page one error 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -2205,9 +2187,7 @@ exports[`renders page one error 1`] = ` version number of the template -

+

For example, 'NHS App - covid19 2023 - over 65s - version 3'

@@ -2263,7 +2243,7 @@ exports[`renders page one error 1`] = ` class="" >

0 of 5000 characters

@@ -2948,11 +2928,9 @@ exports[`renders page with multiple errors 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -2984,9 +2961,7 @@ exports[`renders page with multiple errors 1`] = ` version number of the template -

+

For example, 'NHS App - covid19 2023 - over 65s - version 3'

@@ -3103,7 +3078,7 @@ exports[`renders page with multiple errors 1`] = ` class="" >

0 of 5000 characters

@@ -3701,11 +3676,9 @@ exports[`renders page with preloaded field values 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -3737,9 +3709,7 @@ exports[`renders page with preloaded field values 1`] = ` version number of the template -

+

For example, 'NHS App - covid19 2023 - over 65s - version 3'

@@ -3785,7 +3755,7 @@ exports[`renders page with preloaded field values 1`] = ` class="" >

16 of 5000 characters

@@ -4376,11 +4346,9 @@ exports[`renders page without back link for initial state with id - edit mode 1`

You should name your templates in a way that works best for your service or organisation. @@ -4412,9 +4379,7 @@ exports[`renders page without back link for initial state with id - edit mode 1` version number of the template -

+

For example, 'NHS App - covid19 2023 - over 65s - version 3'

@@ -4460,7 +4425,7 @@ exports[`renders page without back link for initial state with id - edit mode 1` class="" >

16 of 5000 characters

diff --git a/frontend/src/__tests__/components/forms/SmsTemplateForm/SmsTemplateForm.test.tsx b/frontend/src/__tests__/components/forms/SmsTemplateForm/SmsTemplateForm.test.tsx index 125bb6ca0..63d2f95ab 100644 --- a/frontend/src/__tests__/components/forms/SmsTemplateForm/SmsTemplateForm.test.tsx +++ b/frontend/src/__tests__/components/forms/SmsTemplateForm/SmsTemplateForm.test.tsx @@ -121,26 +121,18 @@ describe('CreateSmsTemplate component', () => { /> ); - const templateMessageBox = document.querySelector('#smsTemplateMessage'); - - if (!templateMessageBox) { - throw new Error('Template message box not found'); - } + const templateMessageBox = screen.getByLabelText('Message'); const longMessage = 'x'.repeat(300); await user.type(templateMessageBox, longMessage); - const characterCount = screen.getByTestId('character-message-count-0'); - - if (!characterCount) { - throw new Error('Character count not found'); - } + const characterCount = await screen.findByTestId('character-message-count'); expect(characterCount.textContent).toContain( `${longMessage.length} characters` ); - }, 10_000); + }, 15_000); test('Client-side validation triggers - valid form - no errors', () => { const container = render( diff --git a/frontend/src/__tests__/components/forms/SmsTemplateForm/__snapshots__/SmsTemplateForm.test.tsx.snap b/frontend/src/__tests__/components/forms/SmsTemplateForm/__snapshots__/SmsTemplateForm.test.tsx.snap index 9b0193afb..3fba5f92e 100644 --- a/frontend/src/__tests__/components/forms/SmsTemplateForm/__snapshots__/SmsTemplateForm.test.tsx.snap +++ b/frontend/src/__tests__/components/forms/SmsTemplateForm/__snapshots__/SmsTemplateForm.test.tsx.snap @@ -85,11 +85,9 @@ exports[`CreateSmsTemplate component Client-side validation triggers - invalid f

You should name your templates in a way that works best for your service or organisation. @@ -121,9 +118,7 @@ exports[`CreateSmsTemplate component Client-side validation triggers - invalid f version number of the template -

+

For example, 'SMS - covid19 2023 - over 65s - version 3'

@@ -191,7 +186,7 @@ exports[`CreateSmsTemplate component Client-side validation triggers - invalid f class="" >

0 characters
@@ -200,7 +195,7 @@ exports[`CreateSmsTemplate component Client-side validation triggers - invalid f If you're using personalisation fields, it could be charged as more.

You should name your templates in a way that works best for your service or organisation. @@ -551,9 +543,7 @@ exports[`CreateSmsTemplate component Client-side validation triggers - valid for version number of the template -

+

For example, 'SMS - covid19 2023 - over 65s - version 3'

@@ -599,7 +589,7 @@ exports[`CreateSmsTemplate component Client-side validation triggers - valid for class="" >

16 characters
@@ -608,7 +598,7 @@ exports[`CreateSmsTemplate component Client-side validation triggers - valid for If you're using personalisation fields, it could be charged as more.

You should name your templates in a way that works best for your service or organisation. @@ -984,9 +971,7 @@ exports[`CreateSmsTemplate component renders page one error 1`] = ` version number of the template -

+

For example, 'SMS - covid19 2023 - over 65s - version 3'

@@ -1044,7 +1029,7 @@ exports[`CreateSmsTemplate component renders page one error 1`] = ` class="" >

16 characters
@@ -1053,7 +1038,7 @@ exports[`CreateSmsTemplate component renders page one error 1`] = ` If you're using personalisation fields, it could be charged as more.

You should name your templates in a way that works best for your service or organisation. @@ -1411,9 +1393,7 @@ exports[`CreateSmsTemplate component renders page with back link if initial stat version number of the template -

+

For example, 'SMS - covid19 2023 - over 65s - version 3'

@@ -1459,7 +1439,7 @@ exports[`CreateSmsTemplate component renders page with back link if initial stat class="" >

16 characters
@@ -1468,7 +1448,7 @@ exports[`CreateSmsTemplate component renders page with back link if initial stat If you're using personalisation fields, it could be charged as more.

You should name your templates in a way that works best for your service or organisation. @@ -1906,9 +1883,7 @@ exports[`CreateSmsTemplate component renders page with multiple errors 1`] = ` version number of the template -

+

For example, 'SMS - covid19 2023 - over 65s - version 3'

@@ -2027,7 +2002,7 @@ exports[`CreateSmsTemplate component renders page with multiple errors 1`] = ` class="" >

16 characters
@@ -2036,7 +2011,7 @@ exports[`CreateSmsTemplate component renders page with multiple errors 1`] = ` If you're using personalisation fields, it could be charged as more.

You should name your templates in a way that works best for your service or organisation. @@ -2387,9 +2359,7 @@ exports[`CreateSmsTemplate component renders page with no back link if initial s version number of the template -

+

For example, 'SMS - covid19 2023 - over 65s - version 3'

@@ -2435,7 +2405,7 @@ exports[`CreateSmsTemplate component renders page with no back link if initial s class="" >

16 characters
@@ -2444,7 +2414,7 @@ exports[`CreateSmsTemplate component renders page with no back link if initial s If you're using personalisation fields, it could be charged as more.

{ expect(screen.getByRole('link')).toHaveTextContent('link'); }); - it('passes test ID through if content is a string', () => { - render(); - expect(screen.getByText('This is content')).toHaveAttribute( - 'data-testid', - 'content-id-0' - ); - }); - it('renders multiple segments in correct order', () => { const segments = ['First paragraph', 'Second [link](https://example.com)']; @@ -45,21 +37,6 @@ describe('MarkdownContent', () => { expect(screen.getByRole('link')).toHaveTextContent('link'); }); - it('passes indexed test IDs to each item if content is an array', () => { - render( - - ); - - const first = screen.getByText('First paragraph'); - expect(first).toHaveAttribute('data-testid', 'content-id-0'); - - const second = screen.getByText('Second paragraph'); - expect(second).toHaveAttribute('data-testid', 'content-id-1'); - }); - it('adds correct attributes to links', () => { const segments = ['Click [here](https://example.com)']; diff --git a/frontend/src/__tests__/components/molecules/TemplateNameGuidance.test.tsx b/frontend/src/__tests__/components/molecules/TemplateNameGuidance.test.tsx index ad5504c21..4fd2f0784 100644 --- a/frontend/src/__tests__/components/molecules/TemplateNameGuidance.test.tsx +++ b/frontend/src/__tests__/components/molecules/TemplateNameGuidance.test.tsx @@ -1,30 +1,20 @@ import { render } from '@testing-library/react'; import { TemplateNameGuidance } from '@molecules/TemplateNameGuidance'; -import content from '@content/content'; import { TEMPLATE_TYPE_LIST } from 'nhs-notify-backend-client'; describe('TemplateNameGuidance component', () => { - it('renders component correctly as TemplateNameGuidance', () => { - const container = render(); - - expect(container.asFragment()).toMatchSnapshot(); + it('renders component correctly when template type is not given', () => { + expect(render().asFragment()).toMatchSnapshot(); }); it.each(TEMPLATE_TYPE_LIST)( - 'should correctly display the template naming example when templateType is %s', + 'renders component correctly when template type is %s', (templateType) => { - const expectedText = - content.components.nameYourTemplate.templateNameDetailsExample[ - templateType - ]; - - const container = render( - - ); - expect( - container.getByTestId('template-name-example').textContent - ).toEqual(expectedText); + render( + + ).asFragment() + ).toMatchSnapshot(); } ); }); diff --git a/frontend/src/__tests__/components/molecules/__snapshots__/TemplateNameGuidance.test.tsx.snap b/frontend/src/__tests__/components/molecules/__snapshots__/TemplateNameGuidance.test.tsx.snap index 77161ffa1..11a573423 100644 --- a/frontend/src/__tests__/components/molecules/__snapshots__/TemplateNameGuidance.test.tsx.snap +++ b/frontend/src/__tests__/components/molecules/__snapshots__/TemplateNameGuidance.test.tsx.snap @@ -1,14 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TemplateNameGuidance component renders component correctly as TemplateNameGuidance 1`] = ` +exports[`TemplateNameGuidance component renders component correctly when template type is EMAIL 1`] = `

You should name your templates in a way that works best for your service or organisation. @@ -40,12 +37,187 @@ exports[`TemplateNameGuidance component renders component correctly as TemplateN version number of the template -

+ For example, 'Email - covid19 2023 - over 65s - version 3' +

+
+
+ +`; + +exports[`TemplateNameGuidance component renders component correctly when template type is LETTER 1`] = ` + +
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + message channel it uses +
  • +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Letter - covid19 2023 - over 65s - version 3' +

+
+
+
+`; + +exports[`TemplateNameGuidance component renders component correctly when template type is NHS_APP 1`] = ` + +
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + message channel it uses +
  • +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

For example, 'NHS App - covid19 2023 - over 65s - version 3'

`; + +exports[`TemplateNameGuidance component renders component correctly when template type is SMS 1`] = ` + +
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + message channel it uses +
  • +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'SMS - covid19 2023 - over 65s - version 3' +

+
+
+
+`; + +exports[`TemplateNameGuidance component renders component correctly when template type is not given 1`] = ` + +
+ + + Naming your templates + + +
+

+ You should name your templates in a way that works best for your service or organisation. +

+

+ Common template names include the: +

+
    +
  • + subject or reason for the message +
  • +
  • + intended audience for the template +
  • +
  • + version number of the template +
  • +
+

+ For example, 'Covid19 2025 - over 65s - version 3' +

+
+
+
+`; diff --git a/frontend/src/__tests__/utils/form-data-to-form-state.test.ts b/frontend/src/__tests__/utils/form-data-to-form-state.test.ts new file mode 100644 index 000000000..d4a8cdb39 --- /dev/null +++ b/frontend/src/__tests__/utils/form-data-to-form-state.test.ts @@ -0,0 +1,24 @@ +import { formDataToFormStateFields } from '@utils/form-data-to-form-state'; + +it('returns an object with all of the form data values', () => { + const form = new FormData(); + form.append('foo', 'a'); + form.append('bar', 'b'); + form.append('baz', 'c'); + + expect(formDataToFormStateFields(form)).toEqual({ + foo: 'a', + bar: 'b', + baz: 'c', + }); +}); + +it('removes File objects', () => { + const form = new FormData(); + form.append('foo', 'a'); + form.append('bar', new File(['hello'], 'file.txt')); + + expect(formDataToFormStateFields(form)).toEqual({ + foo: 'a', + }); +}); diff --git a/frontend/src/app/upload-british-sign-language-letter-template/page.tsx b/frontend/src/app/upload-british-sign-language-letter-template/page.tsx new file mode 100644 index 000000000..ab909f5e6 --- /dev/null +++ b/frontend/src/app/upload-british-sign-language-letter-template/page.tsx @@ -0,0 +1,66 @@ +import type { Metadata } from 'next'; +import { redirect, RedirectType } from 'next/navigation'; +import { NHSNotifyBackLink } from '@atoms/NHSNotifyBackLink/NHSNotifyBackLink'; +import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain'; +import copy from '@content/content'; +import * as UploadDocxLetterTemplateForm from '@forms/UploadDocxLetterTemplateForm/form'; +import { ContentRenderer } from '@molecules/ContentRenderer/ContentRenderer'; +import { NHSNotifyFormProvider } from '@providers/form-provider'; +import { fetchClient } from '@utils/server-features'; +import { getCampaignIds } from '@utils/client-config'; +import { uploadBSLLetterTemplate } from './server-action'; + +const content = copy.pages.uploadDocxLetterTemplatePage('q4'); + +export const metadata: Metadata = { + title: content.pageTitle, +}; + +export default async function UploadLargePrintLetterTemplatePage() { + const client = await fetchClient(); + + if (!client?.features.letterAuthoring) { + return redirect('/choose-a-template-type', RedirectType.replace); + } + + const campaignIds = getCampaignIds(client); + + if (campaignIds.length === 0) { + return redirect( + '/upload-letter-template/client-id-and-campaign-id-required', + RedirectType.replace + ); + } + + return ( + <> + + {content.backLink.text} + + + +
+
+

{content.heading}

+
+
+
+
+ + + + + +
+ +
+ +
+
+
+
+ + ); +} diff --git a/frontend/src/app/upload-british-sign-language-letter-template/server-action.ts b/frontend/src/app/upload-british-sign-language-letter-template/server-action.ts new file mode 100644 index 000000000..f059598bc --- /dev/null +++ b/frontend/src/app/upload-british-sign-language-letter-template/server-action.ts @@ -0,0 +1,40 @@ +'use server'; + +import { z } from 'zod/v4'; +import type { FormState } from 'nhs-notify-web-template-management-utils'; +import copy from '@content/content'; +import { formDataToFormStateFields } from '@utils/form-data-to-form-state'; + +const { errors } = copy.components.uploadDocxLetterTemplateForm; + +const $FormSchema = z.object({ + name: z.string(errors.name.empty).nonempty(errors.name.empty), + campaignId: z + .string(errors.campaignId.empty) + .nonempty(errors.campaignId.empty), + file: z + .file(errors.file.empty) + .mime( + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + errors.file.empty + ), +}); + +export async function uploadBSLLetterTemplate( + _: FormState, + form: FormData +): Promise { + const validation = $FormSchema.safeParse(Object.fromEntries(form.entries())); + + const fields = formDataToFormStateFields(form); + + if (validation.error) { + return { + errorState: z.flattenError(validation.error), + fields, + }; + } + + // TODO: CCM-14211 - submit the form and redirect instead of returning + return { fields }; +} diff --git a/frontend/src/app/upload-large-print-letter-template/page.tsx b/frontend/src/app/upload-large-print-letter-template/page.tsx new file mode 100644 index 000000000..ec185aa37 --- /dev/null +++ b/frontend/src/app/upload-large-print-letter-template/page.tsx @@ -0,0 +1,66 @@ +import type { Metadata } from 'next'; +import { redirect, RedirectType } from 'next/navigation'; +import { NHSNotifyBackLink } from '@atoms/NHSNotifyBackLink/NHSNotifyBackLink'; +import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain'; +import copy from '@content/content'; +import * as UploadDocxLetterTemplateForm from '@forms/UploadDocxLetterTemplateForm/form'; +import { ContentRenderer } from '@molecules/ContentRenderer/ContentRenderer'; +import { NHSNotifyFormProvider } from '@providers/form-provider'; +import { fetchClient } from '@utils/server-features'; +import { getCampaignIds } from '@utils/client-config'; +import { uploadLargePrintLetterTemplate } from './server-action'; + +const content = copy.pages.uploadDocxLetterTemplatePage('x1'); + +export const metadata: Metadata = { + title: content.pageTitle, +}; + +export default async function UploadLargePrintLetterTemplatePage() { + const client = await fetchClient(); + + if (!client?.features.letterAuthoring) { + return redirect('/choose-a-template-type', RedirectType.replace); + } + + const campaignIds = getCampaignIds(client); + + if (campaignIds.length === 0) { + return redirect( + '/upload-letter-template/client-id-and-campaign-id-required', + RedirectType.replace + ); + } + + return ( + <> + + {content.backLink.text} + + + +
+
+

{content.heading}

+
+
+
+
+ + + + + +
+ +
+ +
+
+
+
+ + ); +} diff --git a/frontend/src/app/upload-large-print-letter-template/server-action.ts b/frontend/src/app/upload-large-print-letter-template/server-action.ts new file mode 100644 index 000000000..a13f84157 --- /dev/null +++ b/frontend/src/app/upload-large-print-letter-template/server-action.ts @@ -0,0 +1,40 @@ +'use server'; + +import { z } from 'zod/v4'; +import type { FormState } from 'nhs-notify-web-template-management-utils'; +import copy from '@content/content'; +import { formDataToFormStateFields } from '@utils/form-data-to-form-state'; + +const { errors } = copy.components.uploadDocxLetterTemplateForm; + +const $FormSchema = z.object({ + name: z.string(errors.name.empty).nonempty(errors.name.empty), + campaignId: z + .string(errors.campaignId.empty) + .nonempty(errors.campaignId.empty), + file: z + .file(errors.file.empty) + .mime( + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + errors.file.empty + ), +}); + +export async function uploadLargePrintLetterTemplate( + _: FormState, + form: FormData +): Promise { + const validation = $FormSchema.safeParse(Object.fromEntries(form.entries())); + + const fields = formDataToFormStateFields(form); + + if (validation.error) { + return { + errorState: z.flattenError(validation.error), + fields, + }; + } + + // TODO: CCM-14211 - submit the form and redirect instead of returning + return { fields }; +} diff --git a/frontend/src/app/upload-other-language-letter-template/page.tsx b/frontend/src/app/upload-other-language-letter-template/page.tsx new file mode 100644 index 000000000..e9b41b973 --- /dev/null +++ b/frontend/src/app/upload-other-language-letter-template/page.tsx @@ -0,0 +1,67 @@ +import type { Metadata } from 'next'; +import { redirect, RedirectType } from 'next/navigation'; +import { NHSNotifyBackLink } from '@atoms/NHSNotifyBackLink/NHSNotifyBackLink'; +import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain'; +import copy from '@content/content'; +import * as UploadDocxLetterTemplateForm from '@forms/UploadDocxLetterTemplateForm/form'; +import { ContentRenderer } from '@molecules/ContentRenderer/ContentRenderer'; +import { NHSNotifyFormProvider } from '@providers/form-provider'; +import { fetchClient } from '@utils/server-features'; +import { getCampaignIds } from '@utils/client-config'; +import { uploadOtherLanguageLetterTemplate } from './server-action'; + +const content = copy.pages.uploadDocxLetterTemplatePage('language'); + +export const metadata: Metadata = { + title: content.pageTitle, +}; + +export default async function UploadOtherLanguageLetterTemplatePage() { + const client = await fetchClient(); + + if (!client?.features.letterAuthoring) { + return redirect('/choose-a-template-type', RedirectType.replace); + } + + const campaignIds = getCampaignIds(client); + + if (campaignIds.length === 0) { + return redirect( + '/upload-letter-template/client-id-and-campaign-id-required', + RedirectType.replace + ); + } + + return ( + <> + + {content.backLink.text} + + + +
+
+

{content.heading}

+
+
+
+
+ + + + + + +
+ +
+ +
+
+
+
+ + ); +} diff --git a/frontend/src/app/upload-other-language-letter-template/server-action.ts b/frontend/src/app/upload-other-language-letter-template/server-action.ts new file mode 100644 index 000000000..604c91161 --- /dev/null +++ b/frontend/src/app/upload-other-language-letter-template/server-action.ts @@ -0,0 +1,42 @@ +'use server'; + +import { z } from 'zod/v4'; +import { LANGUAGE_LIST } from 'nhs-notify-backend-client'; +import type { FormState } from 'nhs-notify-web-template-management-utils'; +import copy from '@content/content'; +import { formDataToFormStateFields } from '@utils/form-data-to-form-state'; + +const { errors } = copy.components.uploadDocxLetterTemplateForm; + +const $FormSchema = z.object({ + name: z.string(errors.name.empty).nonempty(errors.name.empty), + campaignId: z + .string(errors.campaignId.empty) + .nonempty(errors.campaignId.empty), + language: z.enum(LANGUAGE_LIST, errors.language.empty).exclude(['en']), + file: z + .file(errors.file.empty) + .mime( + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + errors.file.empty + ), +}); + +export async function uploadOtherLanguageLetterTemplate( + _: FormState, + form: FormData +): Promise { + const validation = $FormSchema.safeParse(Object.fromEntries(form.entries())); + + const fields = formDataToFormStateFields(form); + + if (validation.error) { + return { + errorState: z.flattenError(validation.error), + fields, + }; + } + + // TODO: CCM-14211 - submit the form and redirect instead of returning + return { fields }; +} diff --git a/frontend/src/app/upload-standard-english-letter-template/page.tsx b/frontend/src/app/upload-standard-english-letter-template/page.tsx new file mode 100644 index 000000000..425acc552 --- /dev/null +++ b/frontend/src/app/upload-standard-english-letter-template/page.tsx @@ -0,0 +1,66 @@ +import type { Metadata } from 'next'; +import { redirect, RedirectType } from 'next/navigation'; +import { NHSNotifyBackLink } from '@atoms/NHSNotifyBackLink/NHSNotifyBackLink'; +import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain'; +import copy from '@content/content'; +import * as UploadDocxLetterTemplateForm from '@forms/UploadDocxLetterTemplateForm/form'; +import { ContentRenderer } from '@molecules/ContentRenderer/ContentRenderer'; +import { NHSNotifyFormProvider } from '@providers/form-provider'; +import { fetchClient } from '@utils/server-features'; +import { getCampaignIds } from '@utils/client-config'; +import { uploadStandardLetterTemplate } from './server-action'; + +const content = copy.pages.uploadDocxLetterTemplatePage('x0'); + +export const metadata: Metadata = { + title: content.pageTitle, +}; + +export default async function UploadStandardLetterTemplatePage() { + const client = await fetchClient(); + + if (!client?.features.letterAuthoring) { + return redirect('/choose-a-template-type', RedirectType.replace); + } + + const campaignIds = getCampaignIds(client); + + if (campaignIds.length === 0) { + return redirect( + '/upload-letter-template/client-id-and-campaign-id-required', + RedirectType.replace + ); + } + + return ( + <> + + {content.backLink.text} + + + +
+
+

{content.heading}

+
+
+
+
+ + + + + +
+ +
+ +
+
+
+
+ + ); +} diff --git a/frontend/src/app/upload-standard-english-letter-template/server-action.ts b/frontend/src/app/upload-standard-english-letter-template/server-action.ts new file mode 100644 index 000000000..0486826a7 --- /dev/null +++ b/frontend/src/app/upload-standard-english-letter-template/server-action.ts @@ -0,0 +1,40 @@ +'use server'; + +import { z } from 'zod/v4'; +import type { FormState } from 'nhs-notify-web-template-management-utils'; +import copy from '@content/content'; +import { formDataToFormStateFields } from '@utils/form-data-to-form-state'; + +const { errors } = copy.components.uploadDocxLetterTemplateForm; + +const $FormSchema = z.object({ + name: z.string(errors.name.empty).nonempty(errors.name.empty), + campaignId: z + .string(errors.campaignId.empty) + .nonempty(errors.campaignId.empty), + file: z + .file(errors.file.empty) + .mime( + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + errors.file.empty + ), +}); + +export async function uploadStandardLetterTemplate( + _: FormState, + form: FormData +): Promise { + const validation = $FormSchema.safeParse(Object.fromEntries(form.entries())); + + const fields = formDataToFormStateFields(form); + + if (validation.error) { + return { + errorState: z.flattenError(validation.error), + fields, + }; + } + + // TODO: CCM-14211 - submit the form and redirect instead of returning + return { fields }; +} diff --git a/frontend/src/components/atoms/FileUpload/FileUpload.module.scss b/frontend/src/components/atoms/FileUpload/FileUpload.module.scss index 3b19dfe2c..775794e83 100644 --- a/frontend/src/components/atoms/FileUpload/FileUpload.module.scss +++ b/frontend/src/components/atoms/FileUpload/FileUpload.module.scss @@ -1,4 +1,5 @@ .file-upload { + height: auto; width: auto; // The default file upload button in Safari does not diff --git a/frontend/src/components/atoms/FileUpload/FileUpload.tsx b/frontend/src/components/atoms/FileUpload/FileUpload.tsx index b5b32eb73..eb44d3c1f 100644 --- a/frontend/src/components/atoms/FileUpload/FileUpload.tsx +++ b/frontend/src/components/atoms/FileUpload/FileUpload.tsx @@ -8,6 +8,19 @@ interface FileUploadProps extends HTMLProps { hint?: string; } +export function FileUploadInput({ + className, + ...props +}: Omit, 'type'>) { + return ( + + ); +} + const FileUpload: React.FC = ({ error, hint, @@ -26,13 +39,11 @@ const FileUpload: React.FC = ({ {label && } {hint && {hint}} {error && {error}} - ); diff --git a/frontend/src/components/atoms/NHSNotifyBackLink/NHSNotifyBackLink.tsx b/frontend/src/components/atoms/NHSNotifyBackLink/NHSNotifyBackLink.tsx index 1dc325ce0..4a058dfd9 100644 --- a/frontend/src/components/atoms/NHSNotifyBackLink/NHSNotifyBackLink.tsx +++ b/frontend/src/components/atoms/NHSNotifyBackLink/NHSNotifyBackLink.tsx @@ -1,5 +1,6 @@ import React, { HTMLProps } from 'react'; import classNames from 'classnames'; +import Link from 'next/link'; interface NotifyBackLinkProps extends HTMLProps { asElement?: React.ElementType; @@ -24,4 +25,22 @@ function NotifyBackLink({ ); } +type LinkProps = React.ComponentProps; + +export function NHSNotifyBackLink({ + children, + className, + ...props +}: LinkProps) { + return ( + + {children} + + ); +} + export default NotifyBackLink; diff --git a/frontend/src/components/atoms/NHSNotifyFormGroup/NHSNotifyFormGroup.tsx b/frontend/src/components/atoms/NHSNotifyFormGroup/NHSNotifyFormGroup.tsx new file mode 100644 index 000000000..93a14d431 --- /dev/null +++ b/frontend/src/components/atoms/NHSNotifyFormGroup/NHSNotifyFormGroup.tsx @@ -0,0 +1,25 @@ +import classNames from 'classnames'; +import { HTMLProps } from 'react'; + +export function NHSNotifyFormGroup({ + children, + className, + error, + ...props +}: HTMLProps & { error: boolean }) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/forms/EmailTemplateForm/EmailTemplateForm.tsx b/frontend/src/components/forms/EmailTemplateForm/EmailTemplateForm.tsx index eee3cf9c6..b023c0576 100644 --- a/frontend/src/components/forms/EmailTemplateForm/EmailTemplateForm.tsx +++ b/frontend/src/components/forms/EmailTemplateForm/EmailTemplateForm.tsx @@ -109,7 +109,7 @@ export const EmailTemplateForm: FC< {templateNameLabelText} {templateNameHintText} - + {templateNameHintText} - + {templateNameHintText} - + diff --git a/frontend/src/components/forms/SmsTemplateForm/SmsTemplateForm.tsx b/frontend/src/components/forms/SmsTemplateForm/SmsTemplateForm.tsx index fa2d76f16..5278f9000 100644 --- a/frontend/src/components/forms/SmsTemplateForm/SmsTemplateForm.tsx +++ b/frontend/src/components/forms/SmsTemplateForm/SmsTemplateForm.tsx @@ -105,7 +105,7 @@ export const SmsTemplateForm: FC< {templateNameLabelText} {templateNameHintText} - + ) { + const [, action] = useNHSNotifyForm(); + + return ( + + {children} + + + ); +} + +export function NameField() { + const [state] = useNHSNotifyForm(); + + const error = state.errorState?.fieldErrors?.name?.join(','); + + return ( + + + {content.fields.name.hint} + + + {error && {error}} + + + ); +} + +export function CampaignIdField({ campaignIds }: { campaignIds: string[] }) { + const [state] = useNHSNotifyForm(); + + const error = state.errorState?.fieldErrors?.campaignId?.join(','); + + return ( + + + {campaignIds.length === 1 ? ( + <> + {content.fields.campaignId.single.hint} + +

{campaignIds[0]}

+ + ) : ( + <> + {content.fields.campaignId.select.hint} + {error && {error}} + + + )} +
+ ); +} + +export function FileField() { + const [state] = useNHSNotifyForm(); + + const error = state.errorState?.fieldErrors?.file?.join(','); + + return ( + + + + + + {error && {error}} + + + ); +} + +const OTHER_LANGUAGES = LANGUAGE_LIST.filter((language) => language !== 'en'); + +export function LanguageField() { + const [state] = useNHSNotifyForm(); + + const [selectedLanguage, setLanguage] = useState(state.fields?.language); + + const error = state.errorState?.fieldErrors?.language?.join(','); + + return ( + <> + + + + {content.fields.language.hint} + {error && {error}} + + + {isLanguage(selectedLanguage) && isRightToLeft(selectedLanguage) && ( + + + + )} + + ); +} diff --git a/frontend/src/components/molecules/ContentRenderer/ContentRenderer.tsx b/frontend/src/components/molecules/ContentRenderer/ContentRenderer.tsx index de2f66cf0..740f2ca0d 100644 --- a/frontend/src/components/molecules/ContentRenderer/ContentRenderer.tsx +++ b/frontend/src/components/molecules/ContentRenderer/ContentRenderer.tsx @@ -2,21 +2,19 @@ import type { MarkdownToJSX } from 'markdown-to-jsx'; import CodeExample from '@atoms/CodeExample/CodeExample'; import { MarkdownContent } from '@molecules/MarkdownContent/MarkdownContent'; -type StandardBlock = { testId?: string }; - -export type MarkdownTextBlock = StandardBlock & { +export type MarkdownTextBlock = { type: 'text'; text: string; overrides?: MarkdownToJSX.Overrides; }; -export type MarkdownInlineBlock = StandardBlock & { +export type MarkdownInlineBlock = { type: 'inline-text'; text: string; overrides?: MarkdownToJSX.Overrides; }; -export type CodeBlock = StandardBlock & { +export type CodeBlock = { type: 'code'; code: string; aria: { text: string; id: string }; @@ -50,14 +48,11 @@ export function ContentRenderer({ content, variables }: ContentRendererProps) { ); } - const key = block.testId ?? index; - switch (block.type) { case 'text': { return ( diff --git a/frontend/src/components/molecules/MarkdownContent/MarkdownContent.tsx b/frontend/src/components/molecules/MarkdownContent/MarkdownContent.tsx index 5b40846e7..b2cc3ba3c 100644 --- a/frontend/src/components/molecules/MarkdownContent/MarkdownContent.tsx +++ b/frontend/src/components/molecules/MarkdownContent/MarkdownContent.tsx @@ -5,7 +5,6 @@ import Markdown, { type MarkdownToJSX } from 'markdown-to-jsx'; type MarkdownContentProps = { content: string | string[]; variables?: Record; - testId?: string; mode?: 'block' | 'inline'; overrides?: MarkdownToJSX.Overrides; }; @@ -13,7 +12,6 @@ type MarkdownContentProps = { export function MarkdownContent({ content, variables, - testId, mode = 'block', overrides, }: MarkdownContentProps) { @@ -59,7 +57,6 @@ export function MarkdownContent({ {rendered.map((item, index) => ( {item} diff --git a/frontend/src/components/molecules/TemplateNameGuidance/TemplateNameGuidance.tsx b/frontend/src/components/molecules/TemplateNameGuidance/TemplateNameGuidance.tsx index a08464a09..758dc6e1d 100644 --- a/frontend/src/components/molecules/TemplateNameGuidance/TemplateNameGuidance.tsx +++ b/frontend/src/components/molecules/TemplateNameGuidance/TemplateNameGuidance.tsx @@ -1,34 +1,23 @@ -import content from '@content/content'; +import type { HTMLProps } from 'react'; +import type { TemplateType } from 'nhs-notify-backend-client'; import { Details } from 'nhsuk-react-components'; -import { TemplateNameGuidanceType } from './template-name-guidance.types'; - -export function TemplateNameGuidance({ template }: TemplateNameGuidanceType) { - const { - templateNameDetailsSummary, - templateNameDetailsOpeningParagraph, - templateNameDetailsListHeader, - templateNameDetailsList, - templateNameDetailsExample, - } = content.components.nameYourTemplate; +import content from '@content/content'; +import { ContentRenderer } from '@molecules/ContentRenderer/ContentRenderer'; - const templateNameDetailsExampleText = templateNameDetailsExample[template]; +export function TemplateNameGuidance({ + templateType, + ...props +}: Omit, 'children'> & { + templateType?: TemplateType; +}) { + const { summary, text } = + content.components.templateNameGuidance(templateType); return ( -
- - {templateNameDetailsSummary} - - -

{templateNameDetailsOpeningParagraph}

-

{templateNameDetailsListHeader}

-
    - {templateNameDetailsList.map((listItem) => ( -
  • {listItem.text}
  • - ))} -
-

- {templateNameDetailsExampleText} -

+
+ {summary} + +
); diff --git a/frontend/src/components/molecules/TemplateNameGuidance/template-name-guidance.types.ts b/frontend/src/components/molecules/TemplateNameGuidance/template-name-guidance.types.ts deleted file mode 100644 index d7e383ba3..000000000 --- a/frontend/src/components/molecules/TemplateNameGuidance/template-name-guidance.types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { TemplateType } from 'nhs-notify-backend-client'; - -export type TemplateNameGuidanceType = { - template: TemplateType; -}; diff --git a/frontend/src/components/providers/form-provider.tsx b/frontend/src/components/providers/form-provider.tsx index 29326cb45..517e09289 100644 --- a/frontend/src/components/providers/form-provider.tsx +++ b/frontend/src/components/providers/form-provider.tsx @@ -21,6 +21,7 @@ export function useNHSNotifyForm() { throw new Error( 'useNHSNotifyForm must be used within NHSNotifyFormProvider' ); + return context; } diff --git a/frontend/src/content/content.ts b/frontend/src/content/content.ts index 9208ffafb..73a7514e0 100644 --- a/frontend/src/content/content.ts +++ b/frontend/src/content/content.ts @@ -1,8 +1,10 @@ import type { + LetterType, RoutingConfigStatusActive, TemplateStatus, TemplateType, } from 'nhs-notify-backend-client'; + import type { ContentBlock } from '@molecules/ContentRenderer/ContentRenderer'; import { getBasePath } from '@utils/get-base-path'; import { markdownList } from '@utils/markdown-list'; @@ -804,32 +806,49 @@ const chooseTemplateType = { }, }; -const nameYourTemplate = { - templateNameDetailsSummary: 'Naming your templates', - templateNameDetailsOpeningParagraph: - 'You should name your templates in a way that works best for your service or organisation.', - templateNameDetailsListHeader: 'Common template names include the:', - templateNameDetailsList: [ - { id: `template-name-details-item-1`, text: 'message channel it uses' }, - { - id: `template-name-details-item-2`, - text: 'subject or reason for the message', - }, - { - id: `template-name-details-item-3`, - text: 'intended audience for the template', - }, - { - id: `template-name-details-item-4`, - text: 'version number of the template', - }, - ], - templateNameDetailsExample: { - NHS_APP: `For example, 'NHS App - covid19 2023 - over 65s - version 3'`, - EMAIL: `For example, 'Email - covid19 2023 - over 65s - version 3'`, - SMS: `For example, 'SMS - covid19 2023 - over 65s - version 3'`, - LETTER: `For example, 'Letter - covid19 2023 - over 65s - version 3'`, - }, +const templateNameGuidance = (type?: TemplateType) => { + const channelNames: Record = { + NHS_APP: 'NHS App', + EMAIL: 'Email', + SMS: 'SMS', + LETTER: 'Letter', + }; + + const baseNameComponents = [ + 'subject or reason for the message', + 'intended audience for the template', + 'version number of the template', + ]; + + const nameComponentsList = type + ? ['message channel it uses', ...baseNameComponents] + : baseNameComponents; + + const exampleText = type + ? `For example, '${channelNames[type]} - covid19 2023 - over 65s - version 3'` + : `For example, 'Covid19 2025 - over 65s - version 3'`; + + return { + summary: 'Naming your templates', + text: [ + { + type: 'text', + text: 'You should name your templates in a way that works best for your service or organisation.', + }, + { + type: 'text', + text: 'Common template names include the:', + }, + { + type: 'text', + text: markdownList('ul', nameComponentsList), + }, + { + type: 'text', + text: exampleText, + }, + ] satisfies ContentBlock[], + }; }; const channelGuidance = { @@ -1016,13 +1035,25 @@ const templateFormEmail = { const smsTemplateFooter: ContentBlock[] = [ { type: 'text', - testId: 'character-message-count', text: `{{characters}} {{characters|character|characters}} \nThis template will be charged as {{count}} {{count|text message|text messages}}. \nIf you're using personalisation fields, it could be charged as more.`, + overrides: { + p: { + props: { + 'data-testid': 'character-message-count', + }, + }, + }, }, { type: 'text', - testId: 'sms-pricing-info', text: '[Learn more about character counts and text messaging pricing (opens in a new tab)](/pricing/text-messages)', + overrides: { + p: { + props: { + 'data-testid': 'sms-pricing-info', + }, + }, + }, }, ]; @@ -1566,6 +1597,108 @@ const previewMessagePlan = { languageFormatsCardHeading: 'Other language letters (optional)', }; +const uploadDocxLetterTemplateForm = { + fields: { + name: { + label: 'Template name', + hint: 'This will not be visible to recipients.', + }, + campaignId: { + label: 'Campaign', + single: { + hint: 'This message plan will link to your only campaign:', + }, + select: { + hint: 'Choose which campaign this letter is for.', + }, + }, + language: { + label: 'Template language', + hint: 'Choose the language used in this template.', + placeholder: 'Please select', + rtl: [ + { type: 'text', text: '**Right-to-left language selected**' }, + { + type: 'text', + text: "You've selected a language that reads right-to-left. Make sure you use the [other language (right-aligned) letter template file (opens in a new tab)](https://notify.nhs.uk/using-nhs-notify/upload-a-letter).", + }, + ] satisfies ContentBlock[], + }, + file: { + label: 'Template file', + hint: [ + { + type: 'inline-text', + text: 'Only upload your final letter template file. \nMake sure you use one of our blank template files to create the letter.', + }, + ] satisfies ContentBlock[], + }, + submitButton: { + text: 'Upload letter template file', + }, + }, + errors: { + name: { + empty: 'Enter a template name', + }, + campaignId: { + empty: 'Choose a campaign', + }, + file: { + empty: 'Choose a template file', + }, + language: { + empty: 'Choose a language', + }, + }, +}; + +type DocxTemplateType = LetterType | 'language'; + +const docxLetterDisplayMappings: Record = { + x0: 'standard English', + x1: 'large print', + q4: 'British Sign Language', + language: 'other language', +}; + +const article = (noun: string) => (/^[aeiou]/i.test(noun) ? 'an' : 'a'); + +const uploadDocxLetterTemplatePage = (type: DocxTemplateType) => { + const display = docxLetterDisplayMappings[type]; + + return { + pageTitle: generatePageTitle( + `Upload ${article(display)} ${display} letter template` + ), + backLink: { + href: '/choose-a-template-type', + text: 'Back to choose a template type', + }, + heading: `Upload ${article(display)} ${display} letter template`, + sideBar: [ + { + type: 'text', + text: `## How to create ${article(display)} ${display} letter template`, + overrides: { h2: { props: { className: 'nhsuk-heading-m' } } }, + }, + { + type: 'text', + text: markdownList('ol', [ + 'Download the relevant [blank letter template file (opens in a new tab)](https://notify.nhs.uk/using-nhs-notify/upload-a-letter).', + 'Add [formatting (opens in a new tab)](https://notify.nhs.uk/using-nhs-notify/formatting).', + 'Add any [personalisation (opens in a new tab)](https://notify.nhs.uk/using-nhs-notify/personalisation).', + 'Save your Microsoft Word file and upload it to this page.', + ]), + overrides: { + ol: { props: { className: 'nhsuk-list nhsuk-list--number' } }, + li: { props: { className: 'nhsuk-u-margin-bottom-4' } }, + }, + }, + ] satisfies ContentBlock[] as ContentBlock[], + }; +}; + const content = { global: { mainLayout }, components: { @@ -1587,7 +1720,6 @@ const content = { messagePlanFallbackConditions, messagePlanForm, messagePlansListComponent, - nameYourTemplate, personalisation, previewDigitalTemplate, previewEmailTemplate, @@ -1595,6 +1727,7 @@ const content = { previewNHSAppTemplate, previewSMSTemplate, previewTemplateDetails, + previewTemplateFromMessagePlan, requestProof, submitLetterTemplate, submitTemplate, @@ -1602,9 +1735,10 @@ const content = { templateFormLetter, templateFormNhsApp, templateFormSms, + templateNameGuidance, templateSubmitted, + uploadDocxLetterTemplateForm, viewSubmittedTemplate, - previewTemplateFromMessagePlan, }, pages: { chooseEmailTemplate, @@ -1628,6 +1762,7 @@ const content = { previewOtherLanguageLetterTemplate, previewMessagePlan, submitLetterTemplate: submitLetterTemplatePage, + uploadDocxLetterTemplatePage, }, }; diff --git a/frontend/src/utils/form-data-to-form-state.ts b/frontend/src/utils/form-data-to-form-state.ts new file mode 100644 index 000000000..97185963e --- /dev/null +++ b/frontend/src/utils/form-data-to-form-state.ts @@ -0,0 +1,18 @@ +import { FormStateFields } from 'nhs-notify-web-template-management-utils'; + +/** + * Use in server actions to parse a form data object into form state that can be returned to the client when using `useActionState` + * @param formData `FormData` object to parse + * @returns `FormStateFields` + */ +export function formDataToFormStateFields(formData: FormData): FormStateFields { + const fields: FormStateFields = {}; + + for (const [key, value] of formData.entries()) { + if (value !== null && !(value instanceof File)) { + fields[key] = value; + } + } + + return fields; +} diff --git a/lambdas/backend-api/README.md b/lambdas/backend-api/README.md index e9f6d12cb..b9791f7b3 100644 --- a/lambdas/backend-api/README.md +++ b/lambdas/backend-api/README.md @@ -83,8 +83,8 @@ curl -X POST --location "${APIG_STAGE}/v1/template" \ Will create a single letter template. The CSV form part is optional. Do not set a content type header as this must be auto-generated by curl, so that it includes a form-data boundary. ```bash -PDF_PATH="./tests/test-team/fixtures/pdf-upload/with-personalisation/template.pdf" -CSV_PATH="./tests/test-team/fixtures/pdf-upload/with-personalisation/test-data.csv" +PDF_PATH="./tests/test-team/fixtures/letters/with-personalisation/template.pdf" +CSV_PATH="./tests/test-team/fixtures/letters/with-personalisation/test-data.csv" ``` ```bash diff --git a/tests/test-team/fixtures/letters/docx/standard-english-template.docx b/tests/test-team/fixtures/letters/docx/standard-english-template.docx new file mode 100644 index 000000000..2f88b459c Binary files /dev/null and b/tests/test-team/fixtures/letters/docx/standard-english-template.docx differ diff --git a/tests/test-team/fixtures/pdf-upload/multipart-pdf-letter-fixtures.ts b/tests/test-team/fixtures/letters/index.ts similarity index 95% rename from tests/test-team/fixtures/pdf-upload/multipart-pdf-letter-fixtures.ts rename to tests/test-team/fixtures/letters/index.ts index 59712bdaf..6b5ea8e4a 100644 --- a/tests/test-team/fixtures/pdf-upload/multipart-pdf-letter-fixtures.ts +++ b/tests/test-team/fixtures/letters/index.ts @@ -62,3 +62,7 @@ export const pdfUploadFixtures = { ), }, }; + +export const docxFixtures = { + standard: getFile('docx', 'standard-english-template.docx'), +}; diff --git a/tests/test-team/fixtures/pdf-upload/no-custom-personalisation/incomplete-address.pdf b/tests/test-team/fixtures/letters/no-custom-personalisation/incomplete-address.pdf similarity index 100% rename from tests/test-team/fixtures/pdf-upload/no-custom-personalisation/incomplete-address.pdf rename to tests/test-team/fixtures/letters/no-custom-personalisation/incomplete-address.pdf diff --git a/tests/test-team/fixtures/pdf-upload/no-custom-personalisation/password.pdf b/tests/test-team/fixtures/letters/no-custom-personalisation/password.pdf similarity index 100% rename from tests/test-team/fixtures/pdf-upload/no-custom-personalisation/password.pdf rename to tests/test-team/fixtures/letters/no-custom-personalisation/password.pdf diff --git a/tests/test-team/fixtures/pdf-upload/no-custom-personalisation/template.pdf b/tests/test-team/fixtures/letters/no-custom-personalisation/template.pdf similarity index 100% rename from tests/test-team/fixtures/pdf-upload/no-custom-personalisation/template.pdf rename to tests/test-team/fixtures/letters/no-custom-personalisation/template.pdf diff --git a/tests/test-team/fixtures/pdf-upload/with-personalisation/eicar-threat-test.csv b/tests/test-team/fixtures/letters/with-personalisation/eicar-threat-test.csv similarity index 100% rename from tests/test-team/fixtures/pdf-upload/with-personalisation/eicar-threat-test.csv rename to tests/test-team/fixtures/letters/with-personalisation/eicar-threat-test.csv diff --git a/tests/test-team/fixtures/letters/with-personalisation/empty-params.csv b/tests/test-team/fixtures/letters/with-personalisation/empty-params.csv new file mode 100644 index 000000000..b0fdc8457 --- /dev/null +++ b/tests/test-team/fixtures/letters/with-personalisation/empty-params.csv @@ -0,0 +1,2 @@ +Personalisation field,Short length data example,Medium length data example,Long length data example +,short example,middle example,super long example diff --git a/tests/test-team/fixtures/pdf-upload/with-personalisation/empty-params.pdf b/tests/test-team/fixtures/letters/with-personalisation/empty-params.pdf similarity index 100% rename from tests/test-team/fixtures/pdf-upload/with-personalisation/empty-params.pdf rename to tests/test-team/fixtures/letters/with-personalisation/empty-params.pdf diff --git a/tests/test-team/fixtures/letters/with-personalisation/nonsense.csv b/tests/test-team/fixtures/letters/with-personalisation/nonsense.csv new file mode 100644 index 000000000..729e28be0 --- /dev/null +++ b/tests/test-team/fixtures/letters/with-personalisation/nonsense.csv @@ -0,0 +1,2 @@ +Personalisation field,Short length data example,Medium length data example,Long length data example +!!!,short example,middle example,super long example diff --git a/tests/test-team/fixtures/pdf-upload/with-personalisation/nonsense.pdf b/tests/test-team/fixtures/letters/with-personalisation/nonsense.pdf similarity index 100% rename from tests/test-team/fixtures/pdf-upload/with-personalisation/nonsense.pdf rename to tests/test-team/fixtures/letters/with-personalisation/nonsense.pdf diff --git a/tests/test-team/fixtures/pdf-upload/with-personalisation/password.pdf b/tests/test-team/fixtures/letters/with-personalisation/password.pdf similarity index 100% rename from tests/test-team/fixtures/pdf-upload/with-personalisation/password.pdf rename to tests/test-team/fixtures/letters/with-personalisation/password.pdf diff --git a/tests/test-team/fixtures/pdf-upload/with-personalisation/template.pdf b/tests/test-team/fixtures/letters/with-personalisation/template.pdf similarity index 100% rename from tests/test-team/fixtures/pdf-upload/with-personalisation/template.pdf rename to tests/test-team/fixtures/letters/with-personalisation/template.pdf diff --git a/tests/test-team/fixtures/pdf-upload/with-personalisation/test-data.csv b/tests/test-team/fixtures/letters/with-personalisation/test-data.csv similarity index 85% rename from tests/test-team/fixtures/pdf-upload/with-personalisation/test-data.csv rename to tests/test-team/fixtures/letters/with-personalisation/test-data.csv index 9b23d9756..450a8b3e0 100644 --- a/tests/test-team/fixtures/pdf-upload/with-personalisation/test-data.csv +++ b/tests/test-team/fixtures/letters/with-personalisation/test-data.csv @@ -1,4 +1,4 @@ -Personalisation field,Short length data example,Medium length data example,Long length data example +Personalisation field,Short length data example,Medium length data example,Long length data example appointment_date,Monday 1 May 2025,Saturday 10 April 2025,Wednesday 10 September 2025 appointment_time,1:56pm,11:56am,12:56pm appointment_location,"The Epping Breast Screening Unit, St Margaret's Hospital, The Plain, Epping, Essex, CM16 6TN","The Royal Shrewsbury Hospital, Breast Screening Office, Treatment Centre, Mytton Oak Road, Shrewsbury, SY3 8XQ","City, Sandwell & Walsall BSS, The Rosewood Centre, Sandwell & West Birmingham Hospitals NHS Trust, The Birmingham Treatment Centre, City Hospital, Dudley Road, Birmingham, B18 7QH" diff --git a/tests/test-team/fixtures/pdf-upload/with-personalisation/wrong-params.csv b/tests/test-team/fixtures/letters/with-personalisation/wrong-params.csv similarity index 84% rename from tests/test-team/fixtures/pdf-upload/with-personalisation/wrong-params.csv rename to tests/test-team/fixtures/letters/with-personalisation/wrong-params.csv index a761ad50f..83d9cc929 100644 --- a/tests/test-team/fixtures/pdf-upload/with-personalisation/wrong-params.csv +++ b/tests/test-team/fixtures/letters/with-personalisation/wrong-params.csv @@ -1,4 +1,4 @@ -Personalisation field,Short length data example,Medium length data example,Long length data example +Personalisation field,Short length data example,Medium length data example,Long length data example appt_date,Wednesday 10 September 2025,Saturday 10 April 2025,Monday 1 May 2025 appt_time,12:56pm,11:56am,1:56pm appt_location,"City, Sandwell & Walsall BSS, The Rosewood Centre, Sandwell & West Birmingham Hospitals NHS Trust, The Birmingham Treatment Centre, City Hospital, Dudley Road, Birmingham, B18 7QH","The Royal Shrewsbury Hospital, Breast Screening Office, Treatment Centre, Mytton Oak Road, Shrewsbury, SY3 8XQ","The Epping Breast Screening Unit, St Margaret's Hospital, The Plain, Epping, Essex, CM16 6TN" diff --git a/tests/test-team/fixtures/pdf-upload/with-personalisation/empty-params.csv b/tests/test-team/fixtures/pdf-upload/with-personalisation/empty-params.csv deleted file mode 100644 index 1a9e2e4f9..000000000 --- a/tests/test-team/fixtures/pdf-upload/with-personalisation/empty-params.csv +++ /dev/null @@ -1,2 +0,0 @@ -Personalisation field,Short length data example,Medium length data example,Long length data example -,short example,middle example,super long example diff --git a/tests/test-team/fixtures/pdf-upload/with-personalisation/nonsense.csv b/tests/test-team/fixtures/pdf-upload/with-personalisation/nonsense.csv deleted file mode 100644 index afe01f01f..000000000 --- a/tests/test-team/fixtures/pdf-upload/with-personalisation/nonsense.csv +++ /dev/null @@ -1,2 +0,0 @@ -Personalisation field,Short length data example,Medium length data example,Long length data example -!!!,short example,middle example,super long example diff --git a/tests/test-team/helpers/client/client-helper.ts b/tests/test-team/helpers/client/client-helper.ts index 3cc8f5999..f0676ab54 100644 --- a/tests/test-team/helpers/client/client-helper.ts +++ b/tests/test-team/helpers/client/client-helper.ts @@ -34,7 +34,7 @@ export const testClients: TestClients = { }, }, /** - * Client2 has proofing and routing disabled + * Client2 has all feature flags disabled */ Client2: { campaignIds: ['Campaign2'], @@ -56,8 +56,8 @@ export const testClients: TestClients = { name: 'NHS Test Client 4', features: { proofing: true, - routing: false, - letterAuthoring: false, + routing: true, + letterAuthoring: true, }, }, /** @@ -90,7 +90,7 @@ export const testClients: TestClients = { features: { proofing: true, routing: true, - letterAuthoring: false, + letterAuthoring: true, }, }, diff --git a/tests/test-team/pages/email/template-mgmt-create-email-page.ts b/tests/test-team/pages/email/template-mgmt-create-email-page.ts index 5c556f12f..4613decf3 100644 --- a/tests/test-team/pages/email/template-mgmt-create-email-page.ts +++ b/tests/test-team/pages/email/template-mgmt-create-email-page.ts @@ -35,9 +35,9 @@ export class TemplateMgmtCreateEmailPage extends TemplateMgmtBasePage { this.pdsPersonalisationFields = page.locator( '[data-testid="pds-personalisation-fields-details"]' ); - this.namingYourTemplate = page.locator( - '[data-testid="how-to-name-your-template-details"]' - ); + this.namingYourTemplate = page + .getByRole('group') + .filter({ hasText: 'Naming your templates' }); this.messageFormatting = new TemplateMgmtMessageFormatting(page); diff --git a/tests/test-team/pages/email/template-mgmt-edit-email-page.ts b/tests/test-team/pages/email/template-mgmt-edit-email-page.ts index 586e20524..169b768f0 100644 --- a/tests/test-team/pages/email/template-mgmt-edit-email-page.ts +++ b/tests/test-team/pages/email/template-mgmt-edit-email-page.ts @@ -35,9 +35,9 @@ export class TemplateMgmtEditEmailPage extends TemplateMgmtBasePage { this.pdsPersonalisationFields = page.locator( '[data-testid="pds-personalisation-fields-details"]' ); - this.namingYourTemplate = page.locator( - '[data-testid="how-to-name-your-template-details"]' - ); + this.namingYourTemplate = page + .getByRole('group') + .filter({ hasText: 'Naming your templates' }); this.messageFormatting = new TemplateMgmtMessageFormatting(page); diff --git a/tests/test-team/pages/letter/template-mgmt-upload-bsl-letter-template-page.ts b/tests/test-team/pages/letter/template-mgmt-upload-bsl-letter-template-page.ts new file mode 100644 index 000000000..939bfe856 --- /dev/null +++ b/tests/test-team/pages/letter/template-mgmt-upload-bsl-letter-template-page.ts @@ -0,0 +1,33 @@ +import type { Locator, Page } from '@playwright/test'; +import { TemplateMgmtBasePage } from '../template-mgmt-base-page'; + +export class TemplateMgmtUploadBSLLetterTemplatePage extends TemplateMgmtBasePage { + static readonly pathTemplate = + '/upload-british-sign-language-letter-template'; + + nameInput: Locator; + + campaignIdInput: Locator; + + singleCampaignIdText: Locator; + + fileInput: Locator; + + submitButton: Locator; + + constructor(page: Page) { + super(page); + + this.nameInput = page.getByLabel('Template name'); + + this.campaignIdInput = page.getByLabel('Campaign'); + + this.fileInput = page.getByLabel('Template file'); + + this.singleCampaignIdText = page.getByTestId('single-campaign-id-text'); + + this.submitButton = page.getByRole('button', { + name: 'Upload letter template file', + }); + } +} diff --git a/tests/test-team/pages/letter/template-mgmt-upload-large-print-letter-template-page.ts b/tests/test-team/pages/letter/template-mgmt-upload-large-print-letter-template-page.ts new file mode 100644 index 000000000..59c9ef3a3 --- /dev/null +++ b/tests/test-team/pages/letter/template-mgmt-upload-large-print-letter-template-page.ts @@ -0,0 +1,32 @@ +import type { Locator, Page } from '@playwright/test'; +import { TemplateMgmtBasePage } from '../template-mgmt-base-page'; + +export class TemplateMgmtUploadLargePrintLetterTemplatePage extends TemplateMgmtBasePage { + static readonly pathTemplate = '/upload-large-print-letter-template'; + + nameInput: Locator; + + campaignIdInput: Locator; + + singleCampaignIdText: Locator; + + fileInput: Locator; + + submitButton: Locator; + + constructor(page: Page) { + super(page); + + this.nameInput = page.getByLabel('Template name'); + + this.campaignIdInput = page.getByLabel('Campaign'); + + this.fileInput = page.getByLabel('Template file'); + + this.singleCampaignIdText = page.getByTestId('single-campaign-id-text'); + + this.submitButton = page.getByRole('button', { + name: 'Upload letter template file', + }); + } +} diff --git a/tests/test-team/pages/letter/template-mgmt-upload-other-language-letter-template-page.ts b/tests/test-team/pages/letter/template-mgmt-upload-other-language-letter-template-page.ts new file mode 100644 index 000000000..15ebd9bcd --- /dev/null +++ b/tests/test-team/pages/letter/template-mgmt-upload-other-language-letter-template-page.ts @@ -0,0 +1,36 @@ +import type { Locator, Page } from '@playwright/test'; +import { TemplateMgmtBasePage } from '../template-mgmt-base-page'; + +export class TemplateMgmtUploadOtherLanguageLetterTemplatePage extends TemplateMgmtBasePage { + static readonly pathTemplate = '/upload-other-language-letter-template'; + + nameInput: Locator; + + campaignIdInput: Locator; + + singleCampaignIdText: Locator; + + languageInput: Locator; + + fileInput: Locator; + + submitButton: Locator; + + constructor(page: Page) { + super(page); + + this.nameInput = page.getByLabel('Template name'); + + this.campaignIdInput = page.getByLabel('Campaign'); + + this.singleCampaignIdText = page.getByTestId('single-campaign-id-text'); + + this.languageInput = page.getByLabel('Template language'); + + this.fileInput = page.getByLabel('Template file'); + + this.submitButton = page.getByRole('button', { + name: 'Upload letter template file', + }); + } +} diff --git a/tests/test-team/pages/letter/template-mgmt-upload-standard-english-letter-template-page.ts b/tests/test-team/pages/letter/template-mgmt-upload-standard-english-letter-template-page.ts new file mode 100644 index 000000000..66f74e759 --- /dev/null +++ b/tests/test-team/pages/letter/template-mgmt-upload-standard-english-letter-template-page.ts @@ -0,0 +1,32 @@ +import type { Locator, Page } from '@playwright/test'; +import { TemplateMgmtBasePage } from '../template-mgmt-base-page'; + +export class TemplateMgmtUploadStandardEnglishLetterTemplatePage extends TemplateMgmtBasePage { + static readonly pathTemplate = '/upload-standard-english-letter-template'; + + nameInput: Locator; + + campaignIdInput: Locator; + + singleCampaignIdText: Locator; + + fileInput: Locator; + + submitButton: Locator; + + constructor(page: Page) { + super(page); + + this.nameInput = page.getByLabel('Template name'); + + this.campaignIdInput = page.getByLabel('Campaign'); + + this.fileInput = page.getByLabel('Template file'); + + this.singleCampaignIdText = page.getByTestId('single-campaign-id-text'); + + this.submitButton = page.getByRole('button', { + name: 'Upload letter template file', + }); + } +} diff --git a/tests/test-team/pages/nhs-app/template-mgmt-create-nhs-app-page.ts b/tests/test-team/pages/nhs-app/template-mgmt-create-nhs-app-page.ts index 111cc55d2..462a47263 100644 --- a/tests/test-team/pages/nhs-app/template-mgmt-create-nhs-app-page.ts +++ b/tests/test-team/pages/nhs-app/template-mgmt-create-nhs-app-page.ts @@ -29,10 +29,10 @@ export class TemplateMgmtCreateNhsAppPage extends TemplateMgmtBasePage { this.personalisationFields = page.locator( '[data-testid="personalisation-details"]' ); - this.namingYourTemplate = page.locator( - '[data-testid="how-to-name-your-template-details"]' - ); - this.characterCountText = page.getByTestId('character-message-count-0'); + this.namingYourTemplate = page + .getByRole('group') + .filter({ hasText: 'Naming your templates' }); + this.characterCountText = page.getByTestId('character-message-count'); this.messageFormatting = new TemplateMgmtMessageFormatting(page); this.saveAndPreviewButton = page.locator( '[id="create-nhs-app-template-submit-button"]' @@ -44,7 +44,7 @@ export class TemplateMgmtCreateNhsAppPage extends TemplateMgmtBasePage { } async waitForPageToLoad() { - const characterCountLocator = this.page.getByTestId('character-count-0'); + const characterCountLocator = this.page.getByTestId('character-count'); await expect(characterCountLocator).toBeVisible(); } diff --git a/tests/test-team/pages/nhs-app/template-mgmt-edit-nhs-app-page.ts b/tests/test-team/pages/nhs-app/template-mgmt-edit-nhs-app-page.ts index 9f4d70f4b..ce251ac5b 100644 --- a/tests/test-team/pages/nhs-app/template-mgmt-edit-nhs-app-page.ts +++ b/tests/test-team/pages/nhs-app/template-mgmt-edit-nhs-app-page.ts @@ -29,10 +29,10 @@ export class TemplateMgmtEditNhsAppPage extends TemplateMgmtBasePage { this.personalisationFields = page.locator( '[data-testid="personalisation-details"]' ); - this.namingYourTemplate = page.locator( - '[data-testid="how-to-name-your-template-details"]' - ); - this.characterCountText = page.getByTestId('character-message-count-0'); + this.namingYourTemplate = page + .getByRole('group') + .filter({ hasText: 'Naming your templates' }); + this.characterCountText = page.getByTestId('character-message-count'); this.messageFormatting = new TemplateMgmtMessageFormatting(page); this.saveAndPreviewButton = page.locator( @@ -45,7 +45,7 @@ export class TemplateMgmtEditNhsAppPage extends TemplateMgmtBasePage { } async waitForPageToLoad() { - const characterCountLocator = this.page.getByTestId('character-count-0'); + const characterCountLocator = this.page.getByTestId('character-count'); await expect(characterCountLocator).toBeVisible(); } diff --git a/tests/test-team/pages/sms/template-mgmt-create-sms-page.ts b/tests/test-team/pages/sms/template-mgmt-create-sms-page.ts index 1e3ef9197..2c2b52681 100644 --- a/tests/test-team/pages/sms/template-mgmt-create-sms-page.ts +++ b/tests/test-team/pages/sms/template-mgmt-create-sms-page.ts @@ -36,11 +36,11 @@ export class TemplateMgmtCreateSmsPage extends TemplateMgmtBasePage { this.pdsPersonalisationFields = page.locator( '[data-testid="pds-personalisation-fields-details"]' ); - this.namingYourTemplate = page.locator( - '[data-testid="how-to-name-your-template-details"]' - ); - this.pricingLink = page.getByTestId('sms-pricing-info-0').locator('a'); - this.characterCountText = page.getByTestId('character-message-count-0'); + this.namingYourTemplate = page + .getByRole('group') + .filter({ hasText: 'Naming your templates' }); + this.pricingLink = page.getByTestId('sms-pricing-info').locator('a'); + this.characterCountText = page.getByTestId('character-message-count'); this.messageFormatting = new TemplateMgmtMessageFormatting(page); this.saveAndPreviewButton = page.locator( @@ -55,7 +55,7 @@ export class TemplateMgmtCreateSmsPage extends TemplateMgmtBasePage { async waitForPageToLoad() { const characterCountLocator = this.page.locator( - '[data-testid="character-message-count-0"]' + '[data-testid="character-message-count"]' ); await expect(characterCountLocator).toBeVisible(); } diff --git a/tests/test-team/pages/sms/template-mgmt-edit-sms-page.ts b/tests/test-team/pages/sms/template-mgmt-edit-sms-page.ts index 398cb7985..2a738c273 100644 --- a/tests/test-team/pages/sms/template-mgmt-edit-sms-page.ts +++ b/tests/test-team/pages/sms/template-mgmt-edit-sms-page.ts @@ -36,11 +36,11 @@ export class TemplateMgmtEditSmsPage extends TemplateMgmtBasePage { this.pdsPersonalisationFields = page.locator( '[data-testid="pds-personalisation-fields-details"]' ); - this.namingYourTemplate = page.locator( - '[data-testid="how-to-name-your-template-details"]' - ); - this.pricingLink = page.getByTestId('sms-pricing-info-0').locator('a'); - this.characterCountText = page.getByTestId('character-message-count-0'); + this.namingYourTemplate = page + .getByRole('group') + .filter({ hasText: 'Naming your templates' }); + this.pricingLink = page.getByTestId('sms-pricing-info').locator('a'); + this.characterCountText = page.getByTestId('character-message-count'); this.messageFormatting = new TemplateMgmtMessageFormatting(page); this.saveAndPreviewButton = page.locator( @@ -55,7 +55,7 @@ export class TemplateMgmtEditSmsPage extends TemplateMgmtBasePage { async waitForPageToLoad() { const characterCountLocator = this.page.locator( - '[data-testid="character-message-count-0"]' + '[data-testid="character-message-count"]' ); await expect(characterCountLocator).toBeVisible(); } diff --git a/tests/test-team/template-mgmt-api-tests/update-template.api.spec.ts b/tests/test-team/template-mgmt-api-tests/update-template.api.spec.ts index f3590bb2c..8c04e14ae 100644 --- a/tests/test-team/template-mgmt-api-tests/update-template.api.spec.ts +++ b/tests/test-team/template-mgmt-api-tests/update-template.api.spec.ts @@ -10,7 +10,7 @@ import { uuidRegExp, } from 'nhs-notify-web-template-management-test-helper-utils'; import { TemplateAPIPayloadFactory } from '../helpers/factories/template-api-payload-factory'; -import { pdfUploadFixtures } from '../fixtures/pdf-upload/multipart-pdf-letter-fixtures'; +import { pdfUploadFixtures } from '../fixtures/letters'; import { TemplateFactory } from 'helpers/factories/template-factory'; test.describe('PUT /v1/template/:templateId', () => { diff --git a/tests/test-team/template-mgmt-api-tests/upload-letter-template.api.spec.ts b/tests/test-team/template-mgmt-api-tests/upload-letter-template.api.spec.ts index 00d1b5d8a..5efe71cd6 100644 --- a/tests/test-team/template-mgmt-api-tests/upload-letter-template.api.spec.ts +++ b/tests/test-team/template-mgmt-api-tests/upload-letter-template.api.spec.ts @@ -10,7 +10,7 @@ import { isoDateRegExp, uuidRegExp, } from 'nhs-notify-web-template-management-test-helper-utils'; -import { pdfUploadFixtures } from '../fixtures/pdf-upload/multipart-pdf-letter-fixtures'; +import { pdfUploadFixtures } from '../fixtures/letters'; test.describe('POST /v1/letter-template', () => { const authHelper = createAuthHelper(); diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-create-letter-template-page.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-create-letter-template-page.component.spec.ts index 9cd767248..703ee5d5d 100644 --- a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-create-letter-template-page.component.spec.ts +++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-create-letter-template-page.component.spec.ts @@ -87,7 +87,7 @@ test.describe('Upload letter Template Page', () => { await page.locator('input[name="letterTemplatePdf"]').click(); await page .locator('input[name="letterTemplatePdf"]') - .setInputFiles('./fixtures/pdf-upload/with-personalisation/template.pdf'); + .setInputFiles('./fixtures/letters/with-personalisation/template.pdf'); await createTemplatePage.clickSaveAndPreviewButton(); @@ -149,9 +149,7 @@ test.describe('Upload letter Template Page', () => { }); await page .locator('input[name="letterTemplatePdf"]') - .setInputFiles( - './fixtures/pdf-upload/with-personalisation/template.pdf' - ); + .setInputFiles('./fixtures/letters/with-personalisation/template.pdf'); await createTemplatePage.clickSaveAndPreviewButton(); @@ -187,7 +185,7 @@ test.describe('Upload letter Template Page', () => { ).toHaveText(['Select a letter template PDF']); }); - const detailsSections = ['how-to-name-your-template']; + const detailsSections = ['Naming your templates']; for (const section of detailsSections) { // eslint-disable-next-line no-loop-func @@ -197,18 +195,20 @@ test.describe('Upload letter Template Page', () => { const createTemplatePage = new TemplateMgmtUploadLetterPage(page); await createTemplatePage.loadPage(); - await page.getByTestId(`${section}-summary`).click(); - await expect(page.getByTestId(`${section}-details`)).toHaveAttribute( - 'open', - '' - ); - await expect(page.getByTestId(`${section}-text`)).toBeVisible(); + const details = await page + .getByRole('group') + .filter({ hasText: section }); - await page.getByTestId(`${section}-summary`).click(); - await expect(page.getByTestId(`${section}-details`)).not.toHaveAttribute( - 'open' - ); - await expect(page.getByTestId(`${section}-text`)).toBeHidden(); + const summary = details.locator('summary'); + const text = details.locator('.nhsuk-details__text'); + + await summary.click(); + await expect(details).toHaveAttribute('open'); + await expect(text).toBeVisible(); + + await summary.click(); + await expect(details).not.toHaveAttribute('open'); + await expect(text).toBeHidden(); }); } diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-bsl-letter-template.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-bsl-letter-template.component.spec.ts new file mode 100644 index 000000000..f9083aa8e --- /dev/null +++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-bsl-letter-template.component.spec.ts @@ -0,0 +1,183 @@ +import { test, expect } from '@playwright/test'; +import { docxFixtures } from 'fixtures/letters'; +import { + createAuthHelper, + TestUser, + testUsers, +} from 'helpers/auth/cognito-auth-helper'; +import { loginAsUser } from 'helpers/auth/login-as-user'; +import { + assertAndClickBackLinkTop, + assertBackLinkBottomNotPresent, + assertFooterLinks, + assertHeaderLogoLink, + assertSignOutLink, + assertSkipToMainContent, +} from 'helpers/template-mgmt-common.steps'; +import { TemplateMgmtUploadBSLLetterTemplatePage } from 'pages/letter/template-mgmt-upload-bsl-letter-template-page'; + +let userNoCampaignId: TestUser; +let userSingleCampaign: TestUser; +let userMultipleCampaigns: TestUser; +let userAuthoringDisabled: TestUser; + +test.beforeAll(async () => { + const authHelper = createAuthHelper(); + + userSingleCampaign = await authHelper.getTestUser( + testUsers.UserLetterAuthoringEnabled.userId + ); + userNoCampaignId = await authHelper.getTestUser(testUsers.User6.userId); + userMultipleCampaigns = await authHelper.getTestUser( + testUsers.UserWithMultipleCampaigns.userId + ); + userAuthoringDisabled = await authHelper.getTestUser(testUsers.User3.userId); +}); + +test.describe('Upload BSL Letter Template Page', () => { + test.describe('single campaign client', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userSingleCampaign, page); + }); + + test('common page tests', async ({ page, baseURL }) => { + const props = { + page: new TemplateMgmtUploadBSLLetterTemplatePage(page), + baseURL, + }; + + await assertSkipToMainContent(props); + await assertHeaderLogoLink(props); + await assertSignOutLink(props); + await assertFooterLinks(props); + await assertBackLinkBottomNotPresent(props); + await assertAndClickBackLinkTop({ + ...props, + expectedUrl: 'templates/choose-a-template-type', + }); + }); + + test('no validation errors when form is submitted', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadBSLLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(uploadPage.campaignIdInput).toBeHidden(); + await expect(uploadPage.singleCampaignIdText).toHaveText( + userSingleCampaign.campaignIds?.[0] as string + ); + + await uploadPage.nameInput.fill('New Letter Template'); + + await uploadPage.fileInput.click(); + await uploadPage.fileInput.setInputFiles(docxFixtures.standard.filepath); + + await uploadPage.submitButton.click(); + + // TODO: CCM-14211 - test submit behaviour + + await expect(uploadPage.errorSummaryList).toBeHidden(); + }); + + test('displays error messages when blank form is submitted', async ({ + page, + }) => { + const uploadPage = new TemplateMgmtUploadBSLLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(uploadPage.errorSummaryList).toBeHidden(); + + await uploadPage.submitButton.click(); + + await expect(uploadPage.errorSummaryList).toHaveText([ + 'Enter a template name', + 'Choose a template file', + ]); + }); + }); + + test.describe('multi-campaign client', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userMultipleCampaigns, page); + }); + + test('no validation errors when form is submitted', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadBSLLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await uploadPage.nameInput.fill('New Letter Template'); + + await expect(uploadPage.singleCampaignIdText).toBeHidden(); + await uploadPage.campaignIdInput.selectOption( + userMultipleCampaigns.campaignIds?.[0] as string + ); + + await uploadPage.fileInput.click(); + await uploadPage.fileInput.setInputFiles(docxFixtures.standard.filepath); + + await uploadPage.submitButton.click(); + + // TODO: CCM-14211 - test submit behaviour + + await expect(uploadPage.errorSummaryList).toBeHidden(); + }); + + test('displays error messages when blank form is submitted', async ({ + page, + }) => { + const uploadPage = new TemplateMgmtUploadBSLLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(uploadPage.errorSummaryList).toBeHidden(); + + await uploadPage.submitButton.click(); + + await expect(uploadPage.errorSummaryList).toHaveText([ + 'Enter a template name', + 'Choose a campaign', + 'Choose a template file', + ]); + }); + }); + + test.describe('client has no campaign id', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userNoCampaignId, page); + }); + + test('redirects to invalid config page', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadBSLLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(page).toHaveURL( + '/templates/upload-letter-template/client-id-and-campaign-id-required' + ); + }); + }); + + test.describe('client has letter authoring flag disabled', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userAuthoringDisabled, page); + }); + + test('redirects to choose template type page', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadBSLLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(page).toHaveURL('/templates/choose-a-template-type'); + }); + }); +}); diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-large-print-letter-template.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-large-print-letter-template.component.spec.ts new file mode 100644 index 000000000..9807c2cbb --- /dev/null +++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-large-print-letter-template.component.spec.ts @@ -0,0 +1,195 @@ +import { test, expect } from '@playwright/test'; +import { docxFixtures } from 'fixtures/letters'; +import { + createAuthHelper, + TestUser, + testUsers, +} from 'helpers/auth/cognito-auth-helper'; +import { loginAsUser } from 'helpers/auth/login-as-user'; +import { + assertAndClickBackLinkTop, + assertBackLinkBottomNotPresent, + assertFooterLinks, + assertHeaderLogoLink, + assertSignOutLink, + assertSkipToMainContent, +} from 'helpers/template-mgmt-common.steps'; +import { TemplateMgmtUploadLargePrintLetterTemplatePage } from 'pages/letter/template-mgmt-upload-large-print-letter-template-page'; + +let userNoCampaignId: TestUser; +let userSingleCampaign: TestUser; +let userMultipleCampaigns: TestUser; +let userAuthoringDisabled: TestUser; + +test.beforeAll(async () => { + const authHelper = createAuthHelper(); + + userSingleCampaign = await authHelper.getTestUser( + testUsers.UserLetterAuthoringEnabled.userId + ); + userNoCampaignId = await authHelper.getTestUser(testUsers.User6.userId); + userMultipleCampaigns = await authHelper.getTestUser( + testUsers.UserWithMultipleCampaigns.userId + ); + userAuthoringDisabled = await authHelper.getTestUser(testUsers.User3.userId); +}); + +test.describe('Upload Large Print Letter Template Page', () => { + test.describe('single campaign client', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userSingleCampaign, page); + }); + + test('common page tests', async ({ page, baseURL }) => { + const props = { + page: new TemplateMgmtUploadLargePrintLetterTemplatePage(page), + baseURL, + }; + + await assertSkipToMainContent(props); + await assertHeaderLogoLink(props); + await assertSignOutLink(props); + await assertFooterLinks(props); + await assertBackLinkBottomNotPresent(props); + await assertAndClickBackLinkTop({ + ...props, + expectedUrl: 'templates/choose-a-template-type', + }); + }); + + test('no validation errors when form is submitted', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadLargePrintLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(uploadPage.campaignIdInput).toBeHidden(); + await expect(uploadPage.singleCampaignIdText).toHaveText( + userSingleCampaign.campaignIds?.[0] as string + ); + + await uploadPage.nameInput.fill('New Large Print Letter Template'); + + await uploadPage.fileInput.click(); + await uploadPage.fileInput.setInputFiles(docxFixtures.standard.filepath); + + await uploadPage.submitButton.click(); + + // TODO: CCM-14211 - test submit behaviour + + await expect(uploadPage.errorSummaryList).toBeHidden(); + }); + + test('displays error messages when blank form is submitted', async ({ + page, + }) => { + const uploadPage = new TemplateMgmtUploadLargePrintLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(uploadPage.errorSummaryList).toBeHidden(); + + await uploadPage.submitButton.click(); + + await expect(uploadPage.errorSummaryList).toHaveText([ + 'Enter a template name', + 'Choose a template file', + ]); + }); + }); + + test.describe('multi-campaign client', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userMultipleCampaigns, page); + }); + + test('no validation errors when form is submitted', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadLargePrintLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await uploadPage.nameInput.fill('New Large Print Letter Template'); + + await expect(uploadPage.singleCampaignIdText).toBeHidden(); + await uploadPage.campaignIdInput.selectOption( + userMultipleCampaigns.campaignIds?.[0] as string + ); + + await uploadPage.fileInput.click(); + await uploadPage.fileInput.setInputFiles(docxFixtures.standard.filepath); + + await uploadPage.submitButton.click(); + + // TODO: CCM-14211 - test submit behaviour + + await expect(uploadPage.errorSummaryList).toBeHidden(); + }); + + test('displays error messages when blank form is submitted', async ({ + page, + }) => { + const uploadPage = new TemplateMgmtUploadLargePrintLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(uploadPage.errorSummaryList).toBeHidden(); + + await uploadPage.submitButton.click(); + + await expect(uploadPage.errorSummaryList).toHaveText([ + 'Enter a template name', + 'Choose a campaign', + 'Choose a template file', + ]); + }); + }); + + test.describe('client has no campaign id', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userNoCampaignId, page); + }); + + test('redirects to invalid config page', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadLargePrintLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(page).toHaveURL( + '/templates/upload-letter-template/client-id-and-campaign-id-required' + ); + }); + }); + + test.describe('client has letter authoring flag disabled', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userAuthoringDisabled, page); + }); + + test('redirects to choose template type page', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadLargePrintLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(page).toHaveURL('/templates/choose-a-template-type'); + }); + }); +}); diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-other-language-letter-template.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-other-language-letter-template.component.spec.ts new file mode 100644 index 000000000..7c91a6a1f --- /dev/null +++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-other-language-letter-template.component.spec.ts @@ -0,0 +1,201 @@ +import { test, expect } from '@playwright/test'; +import { docxFixtures } from 'fixtures/letters'; +import { + createAuthHelper, + TestUser, + testUsers, +} from 'helpers/auth/cognito-auth-helper'; +import { loginAsUser } from 'helpers/auth/login-as-user'; +import { + assertAndClickBackLinkTop, + assertBackLinkBottomNotPresent, + assertFooterLinks, + assertHeaderLogoLink, + assertSignOutLink, + assertSkipToMainContent, +} from 'helpers/template-mgmt-common.steps'; +import { TemplateMgmtUploadOtherLanguageLetterTemplatePage } from 'pages/letter/template-mgmt-upload-other-language-letter-template-page'; + +let userNoCampaignId: TestUser; +let userSingleCampaign: TestUser; +let userMultipleCampaigns: TestUser; +let userAuthoringDisabled: TestUser; + +test.beforeAll(async () => { + const authHelper = createAuthHelper(); + + userSingleCampaign = await authHelper.getTestUser( + testUsers.UserLetterAuthoringEnabled.userId + ); + userNoCampaignId = await authHelper.getTestUser(testUsers.User6.userId); + userMultipleCampaigns = await authHelper.getTestUser( + testUsers.UserWithMultipleCampaigns.userId + ); + userAuthoringDisabled = await authHelper.getTestUser(testUsers.User3.userId); +}); + +test.describe('Upload Other Language Letter Template Page', () => { + test.describe('single campaign client', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userSingleCampaign, page); + }); + + test('common page tests', async ({ page, baseURL }) => { + const props = { + page: new TemplateMgmtUploadOtherLanguageLetterTemplatePage(page), + baseURL, + }; + + await assertSkipToMainContent(props); + await assertHeaderLogoLink(props); + await assertSignOutLink(props); + await assertFooterLinks(props); + await assertBackLinkBottomNotPresent(props); + await assertAndClickBackLinkTop({ + ...props, + expectedUrl: 'templates/choose-a-template-type', + }); + }); + + test('no validation errors when form is submitted', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadOtherLanguageLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(uploadPage.campaignIdInput).toBeHidden(); + await expect(uploadPage.singleCampaignIdText).toHaveText( + userSingleCampaign.campaignIds?.[0] as string + ); + + await uploadPage.nameInput.fill('New Spanish Letter Template'); + + await uploadPage.languageInput.selectOption('Spanish'); + + await uploadPage.fileInput.click(); + await uploadPage.fileInput.setInputFiles(docxFixtures.standard.filepath); + + await uploadPage.submitButton.click(); + + // TODO: CCM-14211 - test submit behaviour + + await expect(uploadPage.errorSummaryList).toBeHidden(); + }); + + test('displays error messages when blank form is submitted', async ({ + page, + }) => { + const uploadPage = new TemplateMgmtUploadOtherLanguageLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(uploadPage.errorSummaryList).toBeHidden(); + + await uploadPage.submitButton.click(); + + await expect(uploadPage.errorSummaryList).toHaveText([ + 'Enter a template name', + 'Choose a language', + 'Choose a template file', + ]); + }); + }); + + test.describe('multi-campaign client', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userMultipleCampaigns, page); + }); + + test('no validation errors when form is submitted', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadOtherLanguageLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await uploadPage.nameInput.fill('New Spanish Letter Template'); + + await expect(uploadPage.singleCampaignIdText).toBeHidden(); + await uploadPage.campaignIdInput.selectOption( + userMultipleCampaigns.campaignIds?.[0] as string + ); + + await uploadPage.languageInput.selectOption('Spanish'); + + await uploadPage.fileInput.click(); + await uploadPage.fileInput.setInputFiles(docxFixtures.standard.filepath); + + await uploadPage.submitButton.click(); + + // TODO: CCM-14211 - test submit behaviour + + await expect(uploadPage.errorSummaryList).toBeHidden(); + }); + + test('displays error messages when blank form is submitted', async ({ + page, + }) => { + const uploadPage = new TemplateMgmtUploadOtherLanguageLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(uploadPage.errorSummaryList).toBeHidden(); + + await uploadPage.submitButton.click(); + + await expect(uploadPage.errorSummaryList).toHaveText([ + 'Enter a template name', + 'Choose a campaign', + 'Choose a language', + 'Choose a template file', + ]); + }); + }); + + test.describe('client has no campaign id', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userNoCampaignId, page); + }); + + test('redirects to invalid config page', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadOtherLanguageLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(page).toHaveURL( + '/templates/upload-letter-template/client-id-and-campaign-id-required' + ); + }); + }); + + test.describe('client has letter authoring flag disabled', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userAuthoringDisabled, page); + }); + + test('redirects to choose template type page', async ({ page }) => { + const uploadPage = new TemplateMgmtUploadOtherLanguageLetterTemplatePage( + page + ); + + await uploadPage.loadPage(); + + await expect(page).toHaveURL('/templates/choose-a-template-type'); + }); + }); +}); diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-standard-english-letter-template.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-standard-english-letter-template.component.spec.ts new file mode 100644 index 000000000..b65212433 --- /dev/null +++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-standard-english-letter-template.component.spec.ts @@ -0,0 +1,189 @@ +import { test, expect } from '@playwright/test'; +import { docxFixtures } from 'fixtures/letters'; +import { + createAuthHelper, + TestUser, + testUsers, +} from 'helpers/auth/cognito-auth-helper'; +import { loginAsUser } from 'helpers/auth/login-as-user'; +import { + assertAndClickBackLinkTop, + assertBackLinkBottomNotPresent, + assertFooterLinks, + assertHeaderLogoLink, + assertSignOutLink, + assertSkipToMainContent, +} from 'helpers/template-mgmt-common.steps'; +import { TemplateMgmtUploadStandardEnglishLetterTemplatePage } from 'pages/letter/template-mgmt-upload-standard-english-letter-template-page'; + +let userNoCampaignId: TestUser; +let userSingleCampaign: TestUser; +let userMultipleCampaigns: TestUser; +let userAuthoringDisabled: TestUser; + +test.beforeAll(async () => { + const authHelper = createAuthHelper(); + + userSingleCampaign = await authHelper.getTestUser( + testUsers.UserLetterAuthoringEnabled.userId + ); + userNoCampaignId = await authHelper.getTestUser(testUsers.User6.userId); + userMultipleCampaigns = await authHelper.getTestUser( + testUsers.UserWithMultipleCampaigns.userId + ); + userAuthoringDisabled = await authHelper.getTestUser(testUsers.User3.userId); +}); + +test.describe('Upload Standard English Letter Template Page', () => { + test.describe('single campaign client', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userSingleCampaign, page); + }); + + test('common page tests', async ({ page, baseURL }) => { + const props = { + page: new TemplateMgmtUploadStandardEnglishLetterTemplatePage(page), + baseURL, + }; + + await assertSkipToMainContent(props); + await assertHeaderLogoLink(props); + await assertSignOutLink(props); + await assertFooterLinks(props); + await assertBackLinkBottomNotPresent(props); + await assertAndClickBackLinkTop({ + ...props, + expectedUrl: 'templates/choose-a-template-type', + }); + }); + + test('no validation errors when form is submitted', async ({ page }) => { + const uploadPage = + new TemplateMgmtUploadStandardEnglishLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(uploadPage.campaignIdInput).toBeHidden(); + await expect(uploadPage.singleCampaignIdText).toHaveText( + userSingleCampaign.campaignIds?.[0] as string + ); + + await uploadPage.nameInput.fill('New Letter Template'); + + await uploadPage.fileInput.click(); + await uploadPage.fileInput.setInputFiles(docxFixtures.standard.filepath); + + await uploadPage.submitButton.click(); + + // TODO: CCM-14211 - test submit behaviour + + await expect(uploadPage.errorSummaryList).toBeHidden(); + }); + + test('displays error messages when blank form is submitted', async ({ + page, + }) => { + const uploadPage = + new TemplateMgmtUploadStandardEnglishLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(uploadPage.errorSummaryList).toBeHidden(); + + await uploadPage.submitButton.click(); + + await expect(uploadPage.errorSummaryList).toHaveText([ + 'Enter a template name', + 'Choose a template file', + ]); + }); + }); + + test.describe('multi-campaign client', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userMultipleCampaigns, page); + }); + + test('no validation errors when form is submitted', async ({ page }) => { + const uploadPage = + new TemplateMgmtUploadStandardEnglishLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await uploadPage.nameInput.fill('New Letter Template'); + + await expect(uploadPage.singleCampaignIdText).toBeHidden(); + await uploadPage.campaignIdInput.selectOption( + userMultipleCampaigns.campaignIds?.[0] as string + ); + + await uploadPage.fileInput.click(); + await uploadPage.fileInput.setInputFiles(docxFixtures.standard.filepath); + + await uploadPage.submitButton.click(); + + // TODO: CCM-14211 - test submit behaviour + + await expect(uploadPage.errorSummaryList).toBeHidden(); + }); + + test('displays error messages when blank form is submitted', async ({ + page, + }) => { + const uploadPage = + new TemplateMgmtUploadStandardEnglishLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(uploadPage.errorSummaryList).toBeHidden(); + + await uploadPage.submitButton.click(); + + await expect(uploadPage.errorSummaryList).toHaveText([ + 'Enter a template name', + 'Choose a campaign', + 'Choose a template file', + ]); + }); + }); + + test.describe('client has no campaign id', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userNoCampaignId, page); + }); + + test('redirects to invalid config page', async ({ page }) => { + const uploadPage = + new TemplateMgmtUploadStandardEnglishLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(page).toHaveURL( + '/templates/upload-letter-template/client-id-and-campaign-id-required' + ); + }); + }); + + test.describe('client has letter authoring flag disabled', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userAuthoringDisabled, page); + }); + + test('redirects to choose template type page', async ({ page }) => { + const uploadPage = + new TemplateMgmtUploadStandardEnglishLetterTemplatePage(page); + + await uploadPage.loadPage(); + + await expect(page).toHaveURL('/templates/choose-a-template-type'); + }); + }); +}); diff --git a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-create-nhs-app-template-page.component.spec.ts b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-create-nhs-app-template-page.component.spec.ts index 6527253b5..bb1ed51df 100644 --- a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-create-nhs-app-template-page.component.spec.ts +++ b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-create-nhs-app-template-page.component.spec.ts @@ -224,15 +224,15 @@ test.describe('Create NHS App Template Page', () => { }); const detailsSections = [ - 'pds-personalisation-fields', - 'custom-personalisation-fields', - 'line-breaks-and-paragraphs', - 'headings', - 'bold-text', - 'bullet-points', - 'numbered-lists', - 'links-and-urls', - 'how-to-name-your-template', + 'PDS personalisation fields', + 'Custom personalisation fields', + 'Line breaks and paragraphs', + 'Headings', + 'Bold text', + 'Bullet points', + 'Numbered lists', + 'Links and urls', + 'Naming your templates', ]; for (const section of detailsSections) { @@ -243,18 +243,20 @@ test.describe('Create NHS App Template Page', () => { const createTemplatePage = new TemplateMgmtCreateNhsAppPage(page); await createTemplatePage.loadPage(); - await page.getByTestId(`${section}-summary`).click(); - await expect(page.getByTestId(`${section}-details`)).toHaveAttribute( - 'open', - '' - ); - await expect(page.getByTestId(`${section}-text`)).toBeVisible(); - - await page.getByTestId(`${section}-summary`).click(); - await expect(page.getByTestId(`${section}-details`)).not.toHaveAttribute( - 'open' - ); - await expect(page.getByTestId(`${section}-text`)).toBeHidden(); + const details = await page + .getByRole('group') + .filter({ hasText: section }); + + const summary = details.locator('summary'); + const text = details.locator('.nhsuk-details__text'); + + await summary.click(); + await expect(details).toHaveAttribute('open'); + await expect(text).toBeVisible(); + + await summary.click(); + await expect(details).not.toHaveAttribute('open'); + await expect(text).toBeHidden(); }); } diff --git a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-edit-nhs-app-template-page.component.spec.ts b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-edit-nhs-app-template-page.component.spec.ts index 9efcbec85..59f9c0010 100644 --- a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-edit-nhs-app-template-page.component.spec.ts +++ b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-edit-nhs-app-template-page.component.spec.ts @@ -287,15 +287,15 @@ test.describe('Edit NHS App Template Page', () => { }); const detailsSections = [ - 'pds-personalisation-fields', - 'custom-personalisation-fields', - 'line-breaks-and-paragraphs', - 'headings', - 'bold-text', - 'bullet-points', - 'numbered-lists', - 'links-and-urls', - 'how-to-name-your-template', + 'PDS personalisation fields', + 'Custom personalisation fields', + 'Line breaks and paragraphs', + 'Headings', + 'Bold text', + 'Bullet points', + 'Numbered lists', + 'Links and urls', + 'Naming your templates', ]; for (const section of detailsSections) { @@ -313,18 +313,20 @@ test.describe('Edit NHS App Template Page', () => { `${baseURL}/templates/edit-nhs-app-template/${templates.valid.id}` ); - await page.getByTestId(`${section}-summary`).click(); - await expect(page.getByTestId(`${section}-details`)).toHaveAttribute( - 'open', - '' - ); - await expect(page.getByTestId(`${section}-text`)).toBeVisible(); + const details = await page + .getByRole('group') + .filter({ hasText: section }); - await page.getByTestId(`${section}-summary`).click(); - await expect(page.getByTestId(`${section}-details`)).not.toHaveAttribute( - 'open' - ); - await expect(page.getByTestId(`${section}-text`)).toBeHidden(); + const summary = details.locator('summary'); + const text = details.locator('.nhsuk-details__text'); + + await summary.click(); + await expect(details).toHaveAttribute('open'); + await expect(text).toBeVisible(); + + await summary.click(); + await expect(details).not.toHaveAttribute('open'); + await expect(text).toBeHidden(); }); } diff --git a/tests/test-team/template-mgmt-component-tests/template-protected-routes.component.spec.ts b/tests/test-team/template-mgmt-component-tests/template-protected-routes.component.spec.ts index dc46fd157..232ee82a7 100644 --- a/tests/test-team/template-mgmt-component-tests/template-protected-routes.component.spec.ts +++ b/tests/test-team/template-mgmt-component-tests/template-protected-routes.component.spec.ts @@ -9,6 +9,7 @@ import { TemplateMgmtUploadLetterPage } from '../pages/letter/template-mgmt-uplo import { TemplateMgmtCreateNhsAppPage } from '../pages/nhs-app/template-mgmt-create-nhs-app-page'; import { TemplateMgmtCreateSmsPage } from '../pages/sms/template-mgmt-create-sms-page'; import { TemplateMgmtDeletePage } from '../pages/template-mgmt-delete-page'; +import { TemplateMgmtDeleteErrorPage } from 'pages/template-mgmt-delete-error-page'; import { TemplateMgmtEditEmailPage } from '../pages/email/template-mgmt-edit-email-page'; import { TemplateMgmtEditNhsAppPage } from '../pages/nhs-app/template-mgmt-edit-nhs-app-page'; import { TemplateMgmtEditSmsPage } from '../pages/sms/template-mgmt-edit-sms-page'; @@ -32,6 +33,10 @@ import { TemplateMgmtTemplateSubmittedLetterPage } from '../pages/letter/templat import { TemplateMgmtTemplateSubmittedNhsAppPage } from '../pages/nhs-app/template-mgmt-template-submitted-nhs-app-page'; import { TemplateMgmtTemplateSubmittedSmsPage } from '../pages/sms/template-mgmt-template-submitted-sms-page'; import { TemplateMgmtUploadLetterMissingCampaignClientIdPage } from '../pages/letter/template-mgmt-upload-letter-missing-campaign-client-id-page'; +import { TemplateMgmtUploadBSLLetterTemplatePage } from 'pages/letter/template-mgmt-upload-bsl-letter-template-page'; +import { TemplateMgmtUploadLargePrintLetterTemplatePage } from 'pages/letter/template-mgmt-upload-large-print-letter-template-page'; +import { TemplateMgmtUploadOtherLanguageLetterTemplatePage } from 'pages/letter/template-mgmt-upload-other-language-letter-template-page'; +import { TemplateMgmtUploadStandardEnglishLetterTemplatePage } from 'pages/letter/template-mgmt-upload-standard-english-letter-template-page'; import { RoutingChooseMessageOrderPage } from '../pages/routing/choose-message-order-page'; import { RoutingCreateMessagePlanPage } from '../pages/routing/create-message-plan-page'; import { RoutingMessagePlanCampaignIdRequiredPage } from '../pages/routing/campaign-id-required-page'; @@ -53,7 +58,6 @@ import { RoutingPreviewLargePrintLetterTemplatePage } from 'pages/routing/letter import { RoutingPreviewOtherLanguageLetterTemplatePage } from 'pages/routing/letter/preview-other-language-letter-template-page'; import { RoutingGetReadyToMovePage } from 'pages/routing/get-ready-to-move-page'; import { RoutingPreviewMessagePlanPage } from 'pages/routing/preview-message-plan-page'; -import { TemplateMgmtDeleteErrorPage } from 'pages/template-mgmt-delete-error-page'; // Reset storage state for this file to avoid being authenticated test.use({ storageState: { cookies: [], origins: [] } }); @@ -111,6 +115,10 @@ const protectedPages = [ TemplateMgmtTemplateSubmittedSmsPage, TemplateMgmtUploadLetterMissingCampaignClientIdPage, TemplateMgmtUploadLetterPage, + TemplateMgmtUploadBSLLetterTemplatePage, + TemplateMgmtUploadLargePrintLetterTemplatePage, + TemplateMgmtUploadOtherLanguageLetterTemplatePage, + TemplateMgmtUploadStandardEnglishLetterTemplatePage, ]; const publicPages = [TemplateMgmtStartPage]; diff --git a/tests/test-team/template-mgmt-e2e-tests/template-mgmt-letter-file-validation.e2e.spec.ts b/tests/test-team/template-mgmt-e2e-tests/template-mgmt-letter-file-validation.e2e.spec.ts index 0cefa5079..51d008870 100644 --- a/tests/test-team/template-mgmt-e2e-tests/template-mgmt-letter-file-validation.e2e.spec.ts +++ b/tests/test-team/template-mgmt-e2e-tests/template-mgmt-letter-file-validation.e2e.spec.ts @@ -6,7 +6,7 @@ import { testUsers, type TestUser, } from '../helpers/auth/cognito-auth-helper'; -import { pdfUploadFixtures } from '../fixtures/pdf-upload/multipart-pdf-letter-fixtures'; +import { pdfUploadFixtures } from '../fixtures/letters'; import { TemplateMgmtPreviewLetterPage } from '../pages/letter/template-mgmt-preview-letter-page'; test.describe('letter file validation', () => { diff --git a/tests/test-team/template-mgmt-e2e-tests/template-mgmt-letter-full.e2e.spec.ts b/tests/test-team/template-mgmt-e2e-tests/template-mgmt-letter-full.e2e.spec.ts index e1959fe10..7da0dd50d 100644 --- a/tests/test-team/template-mgmt-e2e-tests/template-mgmt-letter-full.e2e.spec.ts +++ b/tests/test-team/template-mgmt-e2e-tests/template-mgmt-letter-full.e2e.spec.ts @@ -6,7 +6,7 @@ import { testUsers, type TestUser, } from '../helpers/auth/cognito-auth-helper'; -import { pdfUploadFixtures } from '../fixtures/pdf-upload/multipart-pdf-letter-fixtures'; +import { pdfUploadFixtures } from '../fixtures/letters'; import { TemplateMgmtPreviewLetterPage } from '../pages/letter/template-mgmt-preview-letter-page'; import { TemplateMgmtSubmitLetterPage } from '../pages/letter/template-mgmt-submit-letter-page'; import { TemplateMgmtTemplateSubmittedLetterPage } from '../pages/letter/template-mgmt-template-submitted-letter-page'; diff --git a/tests/test-team/template-mgmt-e2e-tests/template-mgmt-proof-polling.e2e.spec.ts b/tests/test-team/template-mgmt-e2e-tests/template-mgmt-proof-polling.e2e.spec.ts index ae668633c..31ab90d98 100644 --- a/tests/test-team/template-mgmt-e2e-tests/template-mgmt-proof-polling.e2e.spec.ts +++ b/tests/test-team/template-mgmt-e2e-tests/template-mgmt-proof-polling.e2e.spec.ts @@ -6,7 +6,7 @@ import { } from '../helpers/auth/cognito-auth-helper'; import { TemplateFactory } from '../helpers/factories/template-factory'; import { TemplateStorageHelper } from '../helpers/db/template-storage-helper'; -import { pdfUploadFixtures } from '../fixtures/pdf-upload/multipart-pdf-letter-fixtures'; +import { pdfUploadFixtures } from '../fixtures/letters'; import { SftpHelper } from '../helpers/sftp/sftp-helper'; import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; @@ -41,7 +41,7 @@ test.describe('Letter Proof Polling', () => { // add proofs to SFTP mock const pdfContent = readFileSync( - './fixtures/pdf-upload/no-custom-personalisation/template.pdf' + './fixtures/letters/no-custom-personalisation/template.pdf' ); const supplierReference = [ @@ -158,7 +158,7 @@ test.describe('Letter Proof Polling', () => { // add proofs to SFTP mock const pdfContent = readFileSync( - './fixtures/pdf-upload/no-custom-personalisation/password.pdf' + './fixtures/letters/no-custom-personalisation/password.pdf' ); const supplierReference = [ diff --git a/tests/test-team/template-mgmt-e2e-tests/template-mgmt-proof-request.e2e.spec.ts b/tests/test-team/template-mgmt-e2e-tests/template-mgmt-proof-request.e2e.spec.ts index abd257a74..923027877 100644 --- a/tests/test-team/template-mgmt-e2e-tests/template-mgmt-proof-request.e2e.spec.ts +++ b/tests/test-team/template-mgmt-e2e-tests/template-mgmt-proof-request.e2e.spec.ts @@ -5,7 +5,7 @@ import { testUsers, type TestUser, } from '../helpers/auth/cognito-auth-helper'; -import { pdfUploadFixtures } from '../fixtures/pdf-upload/multipart-pdf-letter-fixtures'; +import { pdfUploadFixtures } from '../fixtures/letters'; import { TemplateFactory } from '../helpers/factories/template-factory'; import path from 'node:path'; import { randomUUID } from 'node:crypto'; diff --git a/tests/test-team/template-mgmt-event-tests/letter-templates.event.spec.ts b/tests/test-team/template-mgmt-event-tests/letter-templates.event.spec.ts index bce8e5bf1..87e6c96c5 100644 --- a/tests/test-team/template-mgmt-event-tests/letter-templates.event.spec.ts +++ b/tests/test-team/template-mgmt-event-tests/letter-templates.event.spec.ts @@ -138,7 +138,7 @@ test.describe('Event publishing - Letters', () => { const start = new Date(); const pdfContent = readFileSync( - './fixtures/pdf-upload/no-custom-personalisation/template.pdf' + './fixtures/letters/no-custom-personalisation/template.pdf' ); const supplierReference = [ diff --git a/utils/utils/src/__tests__/enum.test.ts b/utils/utils/src/__tests__/enum.test.ts index 5f2c91842..c67aa7ad8 100644 --- a/utils/utils/src/__tests__/enum.test.ts +++ b/utils/utils/src/__tests__/enum.test.ts @@ -5,6 +5,7 @@ import { TEMPLATE_STATUS_LIST, TemplateStatus, TemplateType, + LANGUAGE_LIST, LetterVersion, TEMPLATE_TYPE_LIST, } from 'nhs-notify-backend-client'; @@ -34,6 +35,7 @@ import { accessibleFormatDisplayMappings, type SupportedLetterType, createTemplateUrl, + isLanguage, } from '../enum'; describe('templateTypeDisplayMappings', () => { @@ -517,3 +519,13 @@ describe('accessibleFormatDisplayMappings', () => { expect(accessibleFormatDisplayMappings(format)).toEqual(display); }); }); + +describe('isLanguage', () => { + it.each(LANGUAGE_LIST)('returns true when language is %s', (language) => { + expect(isLanguage(language)).toBe(true); + }); + + it('returns false when language is not valid', () => { + expect(isLanguage('not a language')).toBe(false); + }); +}); diff --git a/utils/utils/src/enum.ts b/utils/utils/src/enum.ts index 05085c7b1..052da02d4 100644 --- a/utils/utils/src/enum.ts +++ b/utils/utils/src/enum.ts @@ -62,6 +62,11 @@ const languageMap: Record = { ur: { name: 'Urdu', rtl: true }, zh: { name: 'Chinese', rtl: false }, }; + +export const isLanguage = (value: unknown): value is Language => { + return typeof value === 'string' && Object.keys(languageMap).includes(value); +}; + export const languageMapping = (language: Language) => languageMap[language].name; diff --git a/utils/utils/src/types.ts b/utils/utils/src/types.ts index 88b9a522a..12c3bf1a6 100644 --- a/utils/utils/src/types.ts +++ b/utils/utils/src/types.ts @@ -28,8 +28,13 @@ export type ErrorState = { fieldErrors?: Record; }; +type FormStateFieldValue = string | undefined; + +export type FormStateFields = Record; + export type FormState = { errorState?: ErrorState; + fields?: FormStateFields; }; export type CreateUpdateNHSAppTemplate = Extract<