diff --git a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx index ce868b0c3..77d29b21c 100644 --- a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx @@ -8,11 +8,15 @@ import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import useRole from '../../../helpers/hooks/useRole'; import { actionLinks } from '../../../types/blocks/lloydGeorgeActions'; import { LinkProps } from 'react-router-dom'; + jest.mock('../../../helpers/hooks/useRole'); const mockedUseNavigate = jest.fn(); const mockPdf = buildLgSearchResult(); const mockSetStage = jest.fn(); +const mockSetDownloadRemoveButtonClicked = jest.fn(); +const mockSetError = jest.fn(); +const mockSetFocus = jest.fn(); const mockedUseRole = useRole as jest.Mock; jest.mock('react-router', () => ({ useNavigate: () => mockedUseNavigate, @@ -128,15 +132,7 @@ describe('LloydGeorgeRecordDetails', () => { describe('GP admin non BSOL user', () => { it('renders the record details component with button', () => { - render( - , - ); + renderComponent({ userIsGpAdminNonBSOL: true }); expect(screen.getByText(`Last updated: ${mockPdf.last_updated}`)).toBeInTheDocument(); expect(screen.getByText(`${mockPdf.number_of_files} files`)).toBeInTheDocument(); @@ -151,19 +147,55 @@ describe('LloydGeorgeRecordDetails', () => { expect(screen.queryByText(`Select an action...`)).not.toBeInTheDocument(); expect(screen.queryByTestId('actions-menu')).not.toBeInTheDocument(); }); + + it('set downloadRemoveButtonClicked to true when button is clicked', () => { + renderComponent({ userIsGpAdminNonBSOL: true }); + + const button = screen.getByRole('button', { name: 'Download and remove record' }); + + button.click(); + + expect(mockSetDownloadRemoveButtonClicked).toHaveBeenCalledWith(true); + }); + + it('calls setFocus and setError when the button is clicked again after warning box shown up', () => { + renderComponent({ userIsGpAdminNonBSOL: true, downloadRemoveButtonClicked: true }); + + const button = screen.getByRole('button', { name: 'Download and remove record' }); + + button.click(); + + expect(mockSetError).toHaveBeenCalledWith('confirmDownloadRemove', { + type: 'custom', + message: 'true', + }); + expect(mockSetFocus).toHaveBeenCalledWith('confirmDownloadRemove'); + }); }); }); -const TestApp = (props: Omit) => { - return ; +type mockedProps = Omit< + Props, + 'setStage' | 'stage' | 'setDownloadRemoveButtonClicked' | 'setError' | 'setFocus' +>; +const TestApp = (props: mockedProps) => { + return ( + + ); }; const renderComponent = (propsOverride?: Partial) => { - const props: Omit = { + const props: mockedProps = { lastUpdated: mockPdf.last_updated, numberOfFiles: mockPdf.number_of_files, totalFileSizeInByte: mockPdf.total_file_size_in_byte, - + downloadRemoveButtonClicked: false, ...propsOverride, }; return render(); diff --git a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx index 973885cb1..050c10276 100644 --- a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx @@ -7,6 +7,7 @@ import { Link } from 'react-router-dom'; import useRole from '../../../helpers/hooks/useRole'; import { actionLinks } from '../../../types/blocks/lloydGeorgeActions'; import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; +import { FieldValues, UseFormSetError, UseFormSetFocus } from 'react-hook-form'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; export type Props = { @@ -15,6 +16,10 @@ export type Props = { totalFileSizeInByte: number; setStage: Dispatch>; userIsGpAdminNonBSOL?: boolean; + setDownloadRemoveButtonClicked: Dispatch>; + downloadRemoveButtonClicked: boolean; + setError: UseFormSetError; + setFocus: UseFormSetFocus; }; function LloydGeorgeRecordDetails({ @@ -23,6 +28,10 @@ function LloydGeorgeRecordDetails({ totalFileSizeInByte, setStage, userIsGpAdminNonBSOL, + setDownloadRemoveButtonClicked, + downloadRemoveButtonClicked, + setError, + setFocus, }: Props) { const [showActionsMenu, setShowActionsMenu] = useState(false); const actionsRef = useRef(null); @@ -33,6 +42,15 @@ function LloydGeorgeRecordDetails({ useOnClickOutside(actionsRef, (e) => { setShowActionsMenu(false); }); + + const handleDownloadAndRemoveRecordButton = () => { + if (downloadRemoveButtonClicked) { + setError('confirmDownloadRemove', { type: 'custom', message: 'true' }); + } + setFocus('confirmDownloadRemove'); + setDownloadRemoveButtonClicked(true); + }; + return (
@@ -50,7 +68,10 @@ function LloydGeorgeRecordDetails({
{userIsGpAdminNonBSOL ? (
-
diff --git a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx index fcc2306f2..347096914 100644 --- a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx @@ -12,6 +12,7 @@ import usePatient from '../../../helpers/hooks/usePatient'; import useRole from '../../../helpers/hooks/useRole'; import useIsBSOL from '../../../helpers/hooks/useIsBSOL'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; + const mockPdf = buildLgSearchResult(); const mockPatientDetails = buildPatientDetails(); @@ -22,6 +23,7 @@ const mockedUsePatient = usePatient as jest.Mock; const mockNavigate = jest.fn(); const mockedUseRole = useRole as jest.Mock; const mockedIsBSOL = useIsBSOL as jest.Mock; +const mockSetStage = jest.fn(); jest.mock('react-router', () => ({ useNavigate: () => mockNavigate, @@ -113,18 +115,149 @@ describe('LloydGeorgeRecordStage', () => { expect(screen.getByText('Lloyd George record')).toBeInTheDocument(); }); }); + describe('User is GP admin and non BSOL', () => { + const renderComponentForNonBSOLGPAdmin = () => { + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); + mockedIsBSOL.mockReturnValue(false); + renderComponent(); + }; + + const showConfirmationMessage = async () => { + const greenDownloadButton = screen.getByRole('button', { + name: 'Download and remove record', + }); + + act(() => { + userEvent.click(greenDownloadButton); + }); + await waitFor(() => { + expect( + screen.getByText('Are you sure you want to download and remove this record?'), + ).toBeInTheDocument(); + }); + }; + + const clickRedDownloadButton = () => { + const redDownloadButton = screen.getByRole('button', { + name: 'Yes, download and remove', + }); + + act(() => { + userEvent.click(redDownloadButton); + }); + }; + + it('renders warning callout, header and button', async () => { + renderComponentForNonBSOLGPAdmin(); + + expect(screen.getByText('Before downloading')).toBeInTheDocument(); + expect(screen.getByText('Available records')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Download and remove record' }), + ).toBeInTheDocument(); + }); - it('renders warning callout, header and button when user is GP admin and non BSOL', async () => { - mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); - mockedIsBSOL.mockReturnValue(false); + it('clicking the green download button should show confirmation message, checkbox, red download button and cancel button', async () => { + renderComponentForNonBSOLGPAdmin(); + + const downloadButton = screen.getByRole('button', { + name: 'Download and remove record', + }); + + act(() => { + userEvent.click(downloadButton); + }); + + await waitFor(() => { + expect( + screen.getByText('Are you sure you want to download and remove this record?'), + ).toBeInTheDocument(); + }); + expect( + screen.getByText( + "If you download this record, it removes from our storage. You must keep the patient's record safe.", + ), + ).toBeInTheDocument(); + expect( + screen.getByRole('checkbox', { + name: 'I understand that downloading this record removes it from storage.', + }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Yes, download and remove' }), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + }); - renderComponent(); + it('when checkbox is unchecked, clicking red download button should show an alert and not allowing download', async () => { + renderComponentForNonBSOLGPAdmin(); + await showConfirmationMessage(); + + clickRedDownloadButton(); + + await waitFor(() => { + expect( + screen.getByRole('alert', { name: 'There is a problem' }), + ).toBeInTheDocument(); + }); + expect( + screen.getByText('You must confirm if you want to download and remove this record'), + ).toBeInTheDocument(); + expect( + screen.getByText('Confirm if you want to download and remove this record'), + ).toBeInTheDocument(); + expect(mockSetStage).not.toBeCalled(); + }); - expect(screen.getByText('Before downloading')).toBeInTheDocument(); - expect(screen.getByText('Available records')).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Download and remove record' }), - ).toBeInTheDocument(); + it('when checkbox is checked, clicking red download button should proceed to download and delete process', async () => { + renderComponentForNonBSOLGPAdmin(); + await showConfirmationMessage(); + + act(() => { + userEvent.click(screen.getByRole('checkbox')); + }); + + clickRedDownloadButton(); + + await waitFor(() => { + expect(mockSetStage).toBeCalledWith(LG_RECORD_STAGE.DOWNLOAD_ALL); + }); + }); + + it('when checkbox is toggled 2 times ( = unchecked), red download button should not proceed to download', async () => { + renderComponentForNonBSOLGPAdmin(); + await showConfirmationMessage(); + + const checkBox = screen.getByRole('checkbox'); + act(() => { + userEvent.click(checkBox); + userEvent.click(checkBox); + }); + + clickRedDownloadButton(); + + await waitFor(() => { + expect( + screen.getByRole('alert', { name: 'There is a problem' }), + ).toBeInTheDocument(); + }); + expect(mockSetStage).not.toBeCalled(); + }); + + it('clicking cancel button will hide the confirmation message', async () => { + renderComponentForNonBSOLGPAdmin(); + await showConfirmationMessage(); + + act(() => { + userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + }); + + await waitFor(() => { + expect( + screen.queryByText('Are you sure you want to download and remove this record?'), + ).not.toBeInTheDocument(); + }); + }); }); it('does not render warning callout, header and button when user is GP admin and BSOL', async () => { @@ -167,8 +300,7 @@ describe('LloydGeorgeRecordStage', () => { }); }); const TestApp = (props: Omit) => { - const [stage, setStage] = useState(LG_RECORD_STAGE.RECORD); - return ; + return ; }; const renderComponent = (propsOverride?: Partial) => { diff --git a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx index 58720ae4d..ee10d1c7c 100644 --- a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx @@ -1,6 +1,8 @@ import React, { Dispatch, SetStateAction, useState } from 'react'; import { BackLink, + Button, + ButtonLink, Card, Checkboxes, Details, @@ -10,7 +12,6 @@ import { } from 'nhsuk-react-components'; import { getFormattedDate } from '../../../helpers/utils/formatDate'; import { DOWNLOAD_STAGE } from '../../../types/generic/downloadStage'; -import { Button } from 'nhsuk-react-components'; import PdfViewer from '../../generic/pdfViewer/PdfViewer'; import LloydGeorgeRecordDetails from '../lloydGeorgeRecordDetails/LloydGeorgeRecordDetails'; import { formatNhsNumber } from '../../../helpers/utils/formatNhsNumber'; @@ -20,6 +21,7 @@ import LloydGeorgeRecordError from '../lloydGeorgeRecordError/LloydGeorgeRecordE import useRole from '../../../helpers/hooks/useRole'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import useIsBSOL from '../../../helpers/hooks/useIsBSOL'; +import WarningText from '../../generic/warningText/WarningText'; import ErrorBox from '../../layout/errorBox/ErrorBox'; import { useForm } from 'react-hook-form'; import { InputRef } from '../../../types/generic/inputRef'; @@ -44,17 +46,18 @@ function LloydGeorgeRecordStage({ stage, }: Props) { const [fullScreen, setFullScreen] = useState(false); + const [downloadRemoveButtonClicked, setDownloadRemoveButtonClicked] = useState(false); const patientDetails = usePatient(); const dob: String = patientDetails?.birthDate ? getFormattedDate(new Date(patientDetails.birthDate)) : ''; - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ reValidateMode: 'onSubmit' }); - const { ref: inputRef, ...checkboxProps } = register('confirmBsol', { required: true }); + const { register, handleSubmit, formState, clearErrors, setError, setFocus } = useForm({ + reValidateMode: 'onSubmit', + }); + const { ref: inputRef, ...checkboxProps } = register('confirmDownloadRemove', { + required: true, + }); const nhsNumber: string = patientDetails?.nhsNumber || ''; const formattedNhsNumber = formatNhsNumber(nhsNumber); @@ -72,7 +75,11 @@ function LloydGeorgeRecordStage({ numberOfFiles, totalFileSizeInByte, setStage, - userIsGpAdminNonBSOL: userIsGpAdminNonBSOL, + userIsGpAdminNonBSOL, + setDownloadRemoveButtonClicked, + downloadRemoveButtonClicked, + setError, + setFocus, }; return ; } else { @@ -80,11 +87,20 @@ function LloydGeorgeRecordStage({ } }; + const handleConfirmDownloadAndRemoveButton = () => { + setStage(LG_RECORD_STAGE.DOWNLOAD_ALL); + }; + + const handleCancelButton = () => { + setDownloadRemoveButtonClicked(false); + clearErrors('confirmDownloadRemove'); + }; + return (
- {errors.confirmBsol && ( + {formState.errors.confirmDownloadRemove && ( @@ -102,7 +118,7 @@ function LloydGeorgeRecordStage({ )} {!fullScreen && userIsGpAdminNonBSOL && (
- + Before downloading @@ -115,47 +131,64 @@ function LloydGeorgeRecordStage({ should follow data protection principles as outlined in UK General Data Protection Regulation (GDPR).

- - -
{ - // Success handler here - })} - className={ - errors.confirmBsol - ? 'nhsuk-form-group--error' - : 'nhsuk-form-group' - } - > -
-

- Are you sure you want to download and remove this record? -

- - + +
+

+ Are you sure you want to download and remove this + record? +

+ + - I understand that downloading this record removes it - from storage - - -
- - - - + + I understand that downloading this record removes it + from storage. + +
+
+ + + Cancel + + +
+ )}
-

Available records

)} diff --git a/app/src/styles/App.scss b/app/src/styles/App.scss index cfc3a4135..61b7ce396 100644 --- a/app/src/styles/App.scss +++ b/app/src/styles/App.scss @@ -161,6 +161,25 @@ $govuk-compatibility-govukelements: true; top: 30px; } } + &_gp-admin-non-bsol { + &_inset-text { + padding-bottom: 0; + padding-top: 0; + &_confirm-download-remove-button { + background-color: #b61105; + box-shadow: #9a0c02; + &:hover { + background-color: #9a0c02; + } + &:active { + background-color: #9a0c02; + } + } + &_checkbox { + margin-bottom: 30px; + } + } + } } &_downloadall-stage { &_header { @@ -307,3 +326,8 @@ $govuk-compatibility-govukelements: true; background: $color_nhsuk-yellow; color: $nhsuk-text-color; } + +.govuk-warning-text { + display: flex; + align-items: center; +}