From 91a924bb6fe25d4603a025d7f48b03ff5e302d9a Mon Sep 17 00:00:00 2001 From: Lillie Dae Date: Wed, 28 Jan 2026 11:52:44 +0000 Subject: [PATCH 1/4] [PRMP-1326] Add Review Journey Remove All page --- ...viewDetailsDocumentRemoveAllStage.test.tsx | 334 ++++++++++++++++ .../ReviewDetailsDocumentRemoveAllStage.tsx | 62 +++ .../ReviewDetailsDocumentSelectStage.test.tsx | 363 ++++++++++++++++-- .../ReviewDetailsDocumentSelectStage.tsx | 5 + .../DocumentSelectStage.test.tsx | 210 +++++++++- .../DocumentSelectStage.tsx | 60 ++- .../DocumentUploadRemoveFilesStage.test.tsx | 203 +++++++++- .../DocumentUploadRemoveFilesStage.tsx | 9 +- .../pages/adminRoutesPage/AdminRoutesPage.tsx | 12 + app/src/types/generic/routes.ts | 1 + 10 files changed, 1202 insertions(+), 57 deletions(-) create mode 100644 app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx create mode 100644 app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.tsx diff --git a/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx new file mode 100644 index 0000000000..bd5947acae --- /dev/null +++ b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx @@ -0,0 +1,334 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { afterEach, beforeEach, describe, expect, it, vi, Mock } from 'vitest'; +import ReviewDetailsDocumentRemoveAllStage from './ReviewDetailsDocumentRemoveAllStage'; +import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; +import { ReviewDetails } from '../../../../types/generic/reviews'; +import { + ReviewUploadDocument, + UploadDocumentType, + DOCUMENT_UPLOAD_STATE, +} from '../../../../types/pages/UploadDocumentsPage/types'; +import { DOCUMENT_TYPE } from '../../../../helpers/utils/documentType'; + +const mockNavigate = vi.fn(); +const mockReviewId = 'test-review-789'; +const mockSetDocuments = vi.fn(); + +vi.mock('react-router-dom', async (): Promise => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: (): Mock => mockNavigate, + useParams: (): { reviewId: string } => ({ reviewId: mockReviewId }), + }; +}); + +describe('ReviewDetailsDocumentRemoveAllStage', () => { + const testReviewSnoMed: DOCUMENT_TYPE = '16521000000101' as any; + const mockReviewData: ReviewDetails = new ReviewDetails( + mockReviewId, + testReviewSnoMed, + '2024-01-01', + 'test-uploader', + '2024-01-01', + 'test-reason', + '1', + '1234567890', + ); + + const mockReviewDocument: ReviewUploadDocument = { + file: new File(['test'], 'test-review.pdf', { type: 'application/pdf' }), + id: 'review-doc-1', + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: testReviewSnoMed, + type: UploadDocumentType.REVIEW, + attempts: 0, + }; + + const mockAdditionalDocument: ReviewUploadDocument = { + file: new File(['test'], 'additional.pdf', { type: 'application/pdf' }), + id: 'additional-doc-1', + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: testReviewSnoMed, + type: undefined, + attempts: 0, + }; + + const mockDocuments: ReviewUploadDocument[] = [mockReviewDocument, mockAdditionalDocument]; + + beforeEach(() => { + vi.clearAllMocks(); + import.meta.env.VITE_ENVIRONMENT = 'vitest'; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('Rendering', () => { + it('renders the back link with correct text', () => { + render( + , + ); + + expect(screen.getByTestId('back-button')).toBeInTheDocument(); + expect(screen.getByText('Go back')).toBeInTheDocument(); + }); + + it('renders DocumentUploadRemoveFilesStage component', () => { + render( + , + ); + + expect( + screen.getByRole('heading', { + name: 'Are you sure you want to remove all selected files?', + }), + ).toBeInTheDocument(); + }); + + it('renders remove files button', () => { + render( + , + ); + + expect( + screen.getByRole('button', { name: 'Yes, remove all files' }), + ).toBeInTheDocument(); + }); + + it('renders cancel button', () => { + render( + , + ); + + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + }); + + it('renders spinner when reviewData is null', () => { + render( + , + ); + + expect(screen.getByText('Loading')).toBeInTheDocument(); + }); + }); + + describe('User Interactions', () => { + it('navigates back when back link is clicked', async () => { + render( + , + ); + + const backLink = screen.getByTestId('back-button'); + await userEvent.click(backLink); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(-1); + }); + }); + + it('navigates back when cancel button is clicked', async () => { + render( + , + ); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + await userEvent.click(cancelButton); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(-1); + }); + }); + + it('filters out additional documents when remove all button is clicked', async () => { + render( + , + ); + + const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); + await userEvent.click(removeButton); + + await waitFor(() => { + expect(mockSetDocuments).toHaveBeenCalledWith( + expect.arrayContaining([mockReviewDocument]), + ); + }); + }); + + it('keeps only REVIEW type documents after removal', async () => { + render( + , + ); + + const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); + await userEvent.click(removeButton); + + await waitFor(() => { + const callArgs = mockSetDocuments.mock.calls[0][0]; + expect(callArgs).toHaveLength(1); + expect(callArgs[0].type).toBe(UploadDocumentType.REVIEW); + }); + }); + + it('calls setDocuments and navigates after removal', async () => { + render( + , + ); + + const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); + await userEvent.click(removeButton); + + await waitFor(() => { + expect(mockSetDocuments).toHaveBeenCalled(); + }); + }); + + it('handles empty documents array', async () => { + render( + , + ); + + const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); + await userEvent.click(removeButton); + + await waitFor(() => { + expect(mockSetDocuments).toHaveBeenCalledWith([]); + }); + }); + + it('handles documents with only REVIEW type', async () => { + const reviewOnlyDocs: ReviewUploadDocument[] = [mockReviewDocument]; + + render( + , + ); + + const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); + await userEvent.click(removeButton); + + await waitFor(() => { + expect(mockSetDocuments).toHaveBeenCalledWith([mockReviewDocument]); + }); + }); + + it('handles documents with only additional files', async () => { + const additionalOnlyDocs: ReviewUploadDocument[] = [mockAdditionalDocument]; + + render( + , + ); + + const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); + await userEvent.click(removeButton); + + await waitFor(() => { + expect(mockSetDocuments).toHaveBeenCalledWith([]); + }); + }); + + it('handles multiple additional documents', async () => { + const multipleAdditionalDocs: ReviewUploadDocument[] = [ + mockReviewDocument, + mockAdditionalDocument, + { + ...mockAdditionalDocument, + id: 'additional-doc-2', + file: new File(['test2'], 'additional2.pdf', { type: 'application/pdf' }), + }, + { + ...mockAdditionalDocument, + id: 'additional-doc-3', + file: new File(['test3'], 'additional3.pdf', { type: 'application/pdf' }), + }, + ]; + + render( + , + ); + + const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); + await userEvent.click(removeButton); + + await waitFor(() => { + const callArgs = mockSetDocuments.mock.calls[0][0]; + expect(callArgs).toHaveLength(1); + expect(callArgs[0].type).toBe(UploadDocumentType.REVIEW); + }); + }); + }); + + describe('Accessibility', () => { + it('passes accessibility checks', async () => { + const { container } = render( + , + ); + + const results = await runAxeTest(container); + expect(results).toHaveNoViolations(); + }); + }); +}); diff --git a/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.tsx b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.tsx new file mode 100644 index 0000000000..f21d3ad1ab --- /dev/null +++ b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.tsx @@ -0,0 +1,62 @@ +import React, { Dispatch, JSX, SetStateAction } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { navigateUrlParam, routeChildren } from '../../../../types/generic/routes'; +import { ReviewDetails } from '../../../../types/generic/reviews'; +import { + ReviewUploadDocument, + UploadDocumentType, +} from '../../../../types/pages/UploadDocumentsPage/types'; +import Spinner from '../../../generic/spinner/Spinner'; +import DocumentUploadRemoveFilesStage from '../../_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage'; +import { BackLink } from 'nhsuk-react-components'; + +type ReviewDetailsDocumentRemoveAllStageProps = { + reviewData: ReviewDetails | null; + documents: ReviewUploadDocument[]; + setDocuments: Dispatch>; +}; + +const ReviewDetailsDocumentRemoveAllStage = ({ + reviewData, + documents, + setDocuments, +}: ReviewDetailsDocumentRemoveAllStageProps): JSX.Element => { + const navigate = useNavigate(); + const { reviewId } = useParams<{ reviewId: string }>(); + + if (!reviewData) { + return ; + } + + const handleContinue = (): void => { + setDocuments(documents.filter((doc) => doc.type === UploadDocumentType.REVIEW)); + + navigateUrlParam( + routeChildren.ADMIN_REVIEW_UPLOAD_ADDITIONAL_FILES, + { reviewId: reviewId || '' }, + navigate, + ); + }; + + const handleGoBack = (e: React.MouseEvent): void => { + e.preventDefault(); + navigate(-1); + }; + + return ( + <> + + Go back + + + + + ); +}; + +export default ReviewDetailsDocumentRemoveAllStage; diff --git a/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.test.tsx b/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.test.tsx index 2304b36b2f..fb2ae5d9d3 100644 --- a/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.test.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.test.tsx @@ -1,5 +1,5 @@ import { render, screen, waitFor } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; import ReviewDetailsDocumentSelectStage from './ReviewDetailsDocumentSelectStage'; import { DOCUMENT_TYPE } from '../../../../helpers/utils/documentType'; import { ReviewDetails } from '../../../../types/generic/reviews'; @@ -7,36 +7,71 @@ import { DOCUMENT_UPLOAD_STATE, UploadDocument, } from '../../../../types/pages/UploadDocumentsPage/types'; +import { routeChildren } from '../../../../types/generic/routes'; + +const mockNavigate = vi.fn(); +let capturedOnSuccessOverride: (() => void) | undefined; vi.mock('../../_documentUpload/documentSelectStage/DocumentSelectStage', () => ({ - default: vi.fn(({ documents, setDocuments, documentType, filesErrorRef }) => ( -
-
{documents.length}
-
{documentType}
- -
- {filesErrorRef.current !== undefined ? 'has ref' : 'no ref'} -
-
- )), + default: vi.fn( + ({ + documents, + setDocuments, + documentType, + filesErrorRef, + onSuccessOverride, + backLinkOverride, + removeAllFilesLinkOverride, + isReview, + }) => { + capturedOnSuccessOverride = onSuccessOverride; + return ( +
+
{documents.length}
+
{documentType}
+
{backLinkOverride}
+
+ {removeAllFilesLinkOverride} +
+
{isReview ? 'true' : 'false'}
+ + +
+ {filesErrorRef.current !== undefined ? 'has ref' : 'no ref'} +
+
+ ); + }, + ), })); vi.mock('../../../generic/spinner/Spinner', () => ({ @@ -44,7 +79,7 @@ vi.mock('../../../generic/spinner/Spinner', () => ({ })); vi.mock('react-router-dom', () => ({ - useNavigate: vi.fn(() => vi.fn()), + useNavigate: vi.fn(() => mockNavigate), })); describe('ReviewDetailsDocumentSelectStage', () => { @@ -65,6 +100,11 @@ describe('ReviewDetailsDocumentSelectStage', () => { const mockDocuments: UploadDocument[] = []; const mockSetDocuments = vi.fn(); + beforeEach(() => { + vi.clearAllMocks(); + capturedOnSuccessOverride = undefined; + }); + describe('Rendering', () => { it('shows spinner when reviewData is null', () => { render( @@ -176,4 +216,267 @@ describe('ReviewDetailsDocumentSelectStage', () => { expect(screen.getByTestId('documents-length')).toHaveTextContent('1'); }); }); + + describe('onSuccess callback', () => { + it('navigates to upload file order page when onSuccess is called', async () => { + const testDocuments: UploadDocument[] = [ + { + id: 'test-id', + file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: testReviewSnoMed, + attempts: 0, + numPages: 1, + validated: false, + }, + ]; + + const { rerender } = render( + , + ); + + rerender( + , + ); + + await waitFor(() => { + expect(screen.getByTestId('trigger-success')).toBeInTheDocument(); + }); + + const triggerButton = screen.getByTestId('trigger-success'); + triggerButton.click(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith( + routeChildren.ADMIN_REVIEW_UPLOAD_FILE_ORDER.replaceAll( + ':reviewId', + 'test-review-id.1', + ), + ); + }); + }); + + it('passes onSuccessOverride to DocumentSelectStage', async () => { + const testDocuments: UploadDocument[] = [ + { + id: 'test-id', + file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: testReviewSnoMed, + attempts: 0, + numPages: 1, + validated: false, + }, + ]; + + const { rerender } = render( + , + ); + + rerender( + , + ); + + await waitFor(() => { + expect(capturedOnSuccessOverride).toBeDefined(); + }); + + expect(typeof capturedOnSuccessOverride).toBe('function'); + }); + + it('constructs correct reviewId from review data', async () => { + const customReviewData = new ReviewDetails( + 'custom-id', + testReviewSnoMed, + '2024-01-01T12:00:00Z', + 'Test Uploader', + '2024-01-01T12:00:00Z', + 'Test Reason', + '5', + '1234567890', + ); + customReviewData.files = []; + + const testDocuments: UploadDocument[] = [ + { + id: 'test-id', + file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: testReviewSnoMed, + attempts: 0, + numPages: 1, + validated: false, + }, + ]; + + const { rerender } = render( + , + ); + + rerender( + , + ); + + await waitFor(() => { + expect(screen.getByTestId('trigger-success')).toBeInTheDocument(); + }); + + const triggerButton = screen.getByTestId('trigger-success'); + triggerButton.click(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(expect.stringContaining('custom-id.5')); + }); + }); + }); + + describe('Prop overrides', () => { + it('passes backLinkOverride with correct reviewId', async () => { + const testDocuments: UploadDocument[] = [ + { + id: 'test-id', + file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: testReviewSnoMed, + attempts: 0, + numPages: 1, + validated: false, + }, + ]; + + const { rerender } = render( + , + ); + + rerender( + , + ); + + await waitFor(() => { + expect(screen.getByTestId('back-link-override')).toBeInTheDocument(); + }); + + const expectedBackLink = routeChildren.ADMIN_REVIEW_ADD_MORE_CHOICE.replaceAll( + ':reviewId', + 'test-review-id.1', + ); + expect(screen.getByTestId('back-link-override')).toHaveTextContent(expectedBackLink); + }); + + it('passes removeAllFilesLinkOverride with correct reviewId', async () => { + const testDocuments: UploadDocument[] = [ + { + id: 'test-id', + file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: testReviewSnoMed, + attempts: 0, + numPages: 1, + validated: false, + }, + ]; + + const { rerender } = render( + , + ); + + rerender( + , + ); + + await waitFor(() => { + expect(screen.getByTestId('remove-all-files-link-override')).toBeInTheDocument(); + }); + + const expectedRemoveAllLink = routeChildren.ADMIN_REVIEW_REMOVE_ALL.replaceAll( + ':reviewId', + 'test-review-id.1', + ); + expect(screen.getByTestId('remove-all-files-link-override')).toHaveTextContent( + expectedRemoveAllLink, + ); + }); + + it('passes isReview as true', async () => { + const testDocuments: UploadDocument[] = [ + { + id: 'test-id', + file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: testReviewSnoMed, + attempts: 0, + numPages: 1, + validated: false, + }, + ]; + + const { rerender } = render( + , + ); + + rerender( + , + ); + + await waitFor(() => { + expect(screen.getByTestId('is-review')).toBeInTheDocument(); + }); + + expect(screen.getByTestId('is-review')).toHaveTextContent('true'); + }); + }); }); diff --git a/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.tsx b/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.tsx index 550818c928..df5845b547 100644 --- a/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.tsx @@ -71,6 +71,11 @@ const ReviewDetailsDocumentSelectStage = ({ ':reviewId', `${reviewData?.id}.${reviewData?.version}`, )} + removeAllFilesLinkOverride={routeChildren.ADMIN_REVIEW_REMOVE_ALL.replaceAll( + ':reviewId', + `${reviewData?.id}.${reviewData?.version}`, + )} + isReview={true} /> ); }; diff --git a/app/src/components/blocks/_documentUpload/documentSelectStage/DocumentSelectStage.test.tsx b/app/src/components/blocks/_documentUpload/documentSelectStage/DocumentSelectStage.test.tsx index bc82805517..c2aa2f59bc 100644 --- a/app/src/components/blocks/_documentUpload/documentSelectStage/DocumentSelectStage.test.tsx +++ b/app/src/components/blocks/_documentUpload/documentSelectStage/DocumentSelectStage.test.tsx @@ -16,8 +16,13 @@ import { PDF_PARSING_ERROR_TYPE } from '../../../../helpers/utils/fileUploadErro import { getFormattedDate } from '../../../../helpers/utils/formatDate'; import { formatNhsNumber } from '../../../../helpers/utils/formatNhsNumber'; import { routeChildren, routes } from '../../../../types/generic/routes'; -import { UploadDocument } from '../../../../types/pages/UploadDocumentsPage/types'; -import DocumentSelectStage, { Props } from './DocumentSelectStage'; +import { + UploadDocument, + UploadDocumentType, + ReviewUploadDocument, + DOCUMENT_UPLOAD_STATE, +} from '../../../../types/pages/UploadDocumentsPage/types'; +import DocumentSelectStage from './DocumentSelectStage'; import { getFormattedPatientFullName } from '../../../../helpers/utils/formatPatientFullName'; import { DOCUMENT_TYPE, DOCUMENT_TYPE_CONFIG } from '../../../../helpers/utils/documentType'; @@ -168,7 +173,9 @@ describe('DocumentSelectStage', () => { documentConfig: config, }); - await userEvent.upload(screen.getByTestId('button-input'), [buildLgFile(1)]); + await userEvent.upload(screen.getByTestId('button-input'), [ + buildLgFile(1), + ]); await userEvent.click(await screen.findByTestId('skip-link')); @@ -556,22 +563,215 @@ describe('DocumentSelectStage', () => { }); }); + describe('isReview mode', () => { + const mockReviewDocument: ReviewUploadDocument = { + id: 'review-doc-1', + file: new File(['review'], 'review.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: docConfig.snomedCode as DOCUMENT_TYPE, + attempts: 0, + numPages: 1, + type: UploadDocumentType.REVIEW, + }; + + const mockAdditionalDocument: ReviewUploadDocument = { + id: 'additional-doc-1', + file: new File(['additional'], 'additional.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: docConfig.snomedCode as DOCUMENT_TYPE, + attempts: 0, + numPages: 1, + type: undefined, + }; + + it('renders Remove button for additional documents (non-REVIEW type)', async () => { + renderApp(history, { + isReview: true, + initialDocuments: [mockAdditionalDocument], + }); + + await waitFor(() => { + expect(screen.getByText('additional.pdf')).toBeInTheDocument(); + }); + + const removeButton = screen.getByRole('button', { + name: 'Remove additional.pdf from selection', + }); + expect(removeButton).toBeInTheDocument(); + expect(removeButton).toHaveTextContent('Remove'); + }); + + it('renders dash (-) for review documents (REVIEW type)', async () => { + renderApp(history, { + isReview: true, + initialDocuments: [mockReviewDocument], + }); + + await waitFor(() => { + expect(screen.getByText('review.pdf')).toBeInTheDocument(); + }); + + const removeButton = screen.queryByRole('button', { + name: 'Remove review.pdf from selection', + }); + expect(removeButton).not.toBeInTheDocument(); + + const tableCells = screen.getAllByRole('cell'); + const dashCell = tableCells.find((cell) => cell.textContent === '-'); + expect(dashCell).toBeInTheDocument(); + }); + + it('renders both Remove button and dash for mixed documents', async () => { + renderApp(history, { + isReview: true, + initialDocuments: [mockReviewDocument, mockAdditionalDocument], + }); + + await waitFor(() => { + expect(screen.getByText('review.pdf')).toBeInTheDocument(); + expect(screen.getByText('additional.pdf')).toBeInTheDocument(); + }); + + const removeReviewButton = screen.queryByRole('button', { + name: 'Remove review.pdf from selection', + }); + expect(removeReviewButton).not.toBeInTheDocument(); + + const removeAdditionalButton = screen.getByRole('button', { + name: 'Remove additional.pdf from selection', + }); + expect(removeAdditionalButton).toBeInTheDocument(); + + const tableCells = screen.getAllByRole('cell'); + const dashCell = tableCells.find((cell) => cell.textContent === '-'); + expect(dashCell).toBeInTheDocument(); + }); + + it('allows removing additional documents but not review documents', async () => { + renderApp(history, { + isReview: true, + initialDocuments: [mockReviewDocument, mockAdditionalDocument], + }); + + await waitFor(() => { + expect(screen.getByText('additional.pdf')).toBeInTheDocument(); + }); + + const removeButton = screen.getByRole('button', { + name: 'Remove additional.pdf from selection', + }); + + await userEvent.click(removeButton); + + await waitFor(() => { + expect(screen.queryByText('additional.pdf')).not.toBeInTheDocument(); + }); + + expect(screen.getByText('review.pdf')).toBeInTheDocument(); + }); + + it('navigates to removeAllFilesLinkOverride when Remove all files is clicked in review mode', async () => { + const mockRemoveAllLink = '/admin/reviews/test-123/remove-all'; + + renderApp(history, { + isReview: true, + removeAllFilesLinkOverride: mockRemoveAllLink, + initialDocuments: [mockReviewDocument, mockAdditionalDocument], + }); + + await waitFor(() => { + expect(screen.getByTestId('remove-all-button')).toBeInTheDocument(); + }); + + const removeAllButton = screen.getByTestId('remove-all-button'); + await userEvent.click(removeAllButton); + + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(mockRemoveAllLink); + }); + }); + }); + + describe('non-review mode', () => { + it('renders Remove button for all documents when isReview is false', async () => { + const mockDocument: UploadDocument = { + id: 'doc-1', + file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: docConfig.snomedCode as DOCUMENT_TYPE, + attempts: 0, + numPages: 1, + }; + + renderApp(history, { + isReview: false, + initialDocuments: [mockDocument], + }); + + await waitFor(() => { + expect(screen.getByText('test.pdf')).toBeInTheDocument(); + }); + + const removeButton = screen.getByRole('button', { + name: 'Remove test.pdf from selection', + }); + expect(removeButton).toBeInTheDocument(); + expect(removeButton).toHaveTextContent('Remove'); + }); + + it('does not show dash when isReview is false', async () => { + const mockDocument: UploadDocument = { + id: 'doc-1', + file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + progress: 0, + docType: docConfig.snomedCode as DOCUMENT_TYPE, + attempts: 0, + numPages: 1, + }; + + renderApp(history, { + isReview: false, + initialDocuments: [mockDocument], + }); + + await waitFor(() => { + expect(screen.getByText('test.pdf')).toBeInTheDocument(); + }); + + const tableCells = screen.getAllByRole('cell'); + const dashCell = tableCells.find((cell) => cell.textContent === '-'); + expect(dashCell).toBeUndefined(); + }); + }); + type TestAppProps = { goToPreviousDocType?: () => void; goToNextDocType?: () => void; backLinkOverride?: string; + removeAllFilesLinkOverride?: string; showSkipLink?: boolean; documentConfig?: DOCUMENT_TYPE_CONFIG; + isReview?: boolean; + initialDocuments?: Array; }; const TestApp = ({ goToPreviousDocType, goToNextDocType, backLinkOverride, + removeAllFilesLinkOverride, showSkipLink, documentConfig, + isReview, + initialDocuments, }: TestAppProps): JSX.Element => { - const [documents, setDocuments] = useState>([]); + const [documents, setDocuments] = useState>( + initialDocuments ?? [], + ); const filesErrorRef = useRef(false); return ( @@ -585,6 +785,8 @@ describe('DocumentSelectStage', () => { goToNextDocType={goToNextDocType} showSkiplink={showSkipLink} backLinkOverride={backLinkOverride} + removeAllFilesLinkOverride={removeAllFilesLinkOverride} + isReview={isReview} /> ); }; diff --git a/app/src/components/blocks/_documentUpload/documentSelectStage/DocumentSelectStage.tsx b/app/src/components/blocks/_documentUpload/documentSelectStage/DocumentSelectStage.tsx index 06c31f3da1..3ee4ff0bdf 100644 --- a/app/src/components/blocks/_documentUpload/documentSelectStage/DocumentSelectStage.tsx +++ b/app/src/components/blocks/_documentUpload/documentSelectStage/DocumentSelectStage.tsx @@ -21,8 +21,10 @@ import { routeChildren, routes } from '../../../../types/generic/routes'; import { DOCUMENT_UPLOAD_STATE, FileInputEvent, + ReviewUploadDocument, SetUploadDocuments, UploadDocument, + UploadDocumentType, } from '../../../../types/pages/UploadDocumentsPage/types'; import LinkButton from '../../../generic/linkButton/LinkButton'; import PatientSummary, { PatientInfo } from '../../../generic/patientSummary/PatientSummary'; @@ -35,15 +37,17 @@ import parseTextWithLinks from '../../../../helpers/utils/parseTextWithLinks'; export type Props = { setDocuments: SetUploadDocuments; - documents: Array; + documents: UploadDocument[] | ReviewUploadDocument[]; documentType: DOCUMENT_TYPE; filesErrorRef: RefObject; documentConfig: DOCUMENT_TYPE_CONFIG; onSuccessOverride?: () => void; backLinkOverride?: string; + removeAllFilesLinkOverride?: string; goToNextDocType?: () => void; goToPreviousDocType?: () => void; showSkiplink?: boolean; + isReview?: boolean; }; type UploadFilesError = ErrorMessageListItem; @@ -56,9 +60,11 @@ const DocumentSelectStage = ({ documentConfig, onSuccessOverride, backLinkOverride, + removeAllFilesLinkOverride, goToNextDocType, goToPreviousDocType, showSkiplink, + isReview = false, }: Props): JSX.Element => { const fileInputRef = useRef(null); const [noFilesSelected, setNoFilesSelected] = useState(false); @@ -293,25 +299,47 @@ const DocumentSelectStage = ({ resetErrors(); }; - const DocumentRow = (document: UploadDocument, index: number): JSX.Element => { + const DocumentRow = ( + document: UploadDocument | ReviewUploadDocument, + index: number, + ): JSX.Element => { + const isReviewDocument = + isReview && (document as ReviewUploadDocument).type !== UploadDocumentType.REVIEW; return ( {document.file.name} {formatFileSize(document.file.size)} - - - + {isReviewDocument && ( + + + + )} + {isReview && !isReviewDocument && -} + {!isReview && ( + + + + )} ); }; @@ -551,6 +579,10 @@ const DocumentSelectStage = ({ className="remove-all-button mb-5" data-testid="remove-all-button" onClick={(): void => { + if (isReview && removeAllFilesLinkOverride) { + navigate(removeAllFilesLinkOverride); + return; + } navigate.withParams(routeChildren.DOCUMENT_UPLOAD_REMOVE_ALL); }} > diff --git a/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.test.tsx b/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.test.tsx index 64cfc7093a..722d173e6e 100644 --- a/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.test.tsx +++ b/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.test.tsx @@ -1,41 +1,228 @@ import { render, waitFor, screen } from '@testing-library/react'; -import { UploadDocument } from '../../../../types/pages/UploadDocumentsPage/types'; +import userEvent from '@testing-library/user-event'; +import { + UploadDocument, + DOCUMENT_UPLOAD_STATE, +} from '../../../../types/pages/UploadDocumentsPage/types'; import DocumentUploadRemoveFilesStage from './DocumentUploadRemoveFilesStage'; import { DOCUMENT_TYPE } from '../../../../helpers/utils/documentType'; +import { routes } from '../../../../types/generic/routes'; const mockNavigate = vi.fn(); +const mockWithParams = vi.fn(); + vi.mock('../../../../helpers/hooks/usePatient'); -vi.mock('react-router-dom', () => ({ - useNavigate: () => mockNavigate, +vi.mock('../../../../helpers/utils/urlManipulations', () => ({ + useEnhancedNavigate: (): { + (delta: number): void; + withParams: typeof mockWithParams; + } => { + const nav = mockNavigate as unknown as (delta: number) => void; + return Object.assign(nav, { withParams: mockWithParams }); + }, })); URL.createObjectURL = vi.fn(); -describe('DocumentUploadCompleteStage', () => { +describe('DocumentUploadRemoveFilesStage', () => { let documents: UploadDocument[]; + const mockSetDocuments = vi.fn(); + beforeEach(() => { import.meta.env.VITE_ENVIRONMENT = 'vitest'; - documents = []; + documents = [ + { + id: '1', + file: new File(['test'], 'test1.pdf'), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + docType: DOCUMENT_TYPE.LLOYD_GEORGE, + attempts: 0, + }, + { + id: '2', + file: new File(['test'], 'test2.pdf'), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + docType: DOCUMENT_TYPE.EHR, + attempts: 0, + }, + { + id: '3', + file: new File(['test'], 'test3.pdf'), + state: DOCUMENT_UPLOAD_STATE.SELECTED, + docType: DOCUMENT_TYPE.LLOYD_GEORGE, + attempts: 0, + }, + ]; }); + afterEach(() => { vi.clearAllMocks(); }); describe('Rendering', () => { - it('renders', async () => { + it('renders the confirmation page with correct title', async () => { render( {}} + setDocuments={mockSetDocuments} documentType={DOCUMENT_TYPE.LLOYD_GEORGE} />, ); - await waitFor(async () => { + await waitFor(() => { expect( screen.getByText('Are you sure you want to remove all selected files?'), ).toBeInTheDocument(); }); }); + + it('renders remove files button', () => { + render( + , + ); + + expect(screen.getByTestId('remove-files-button')).toBeInTheDocument(); + expect(screen.getByText('Yes, remove all files')).toBeInTheDocument(); + }); + + it('renders cancel button', () => { + render( + , + ); + + expect(screen.getByText('Cancel')).toBeInTheDocument(); + }); + }); + + describe('Remove files button functionality', () => { + it('calls onSuccess callback when provided and does not call setDocuments or navigate', async () => { + const mockOnSuccess = vi.fn(); + const user = userEvent.setup(); + + render( + , + ); + + const removeButton = screen.getByTestId('remove-files-button'); + await user.click(removeButton); + + expect(mockOnSuccess).toHaveBeenCalledTimes(1); + expect(mockSetDocuments).not.toHaveBeenCalled(); + expect(mockWithParams).not.toHaveBeenCalled(); + }); + + it('filters documents by type and navigates when onSuccess is not provided', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const removeButton = screen.getByTestId('remove-files-button'); + await user.click(removeButton); + + expect(mockSetDocuments).toHaveBeenCalledTimes(1); + const filteredDocuments = mockSetDocuments.mock.calls[0][0]; + expect(filteredDocuments).toHaveLength(1); + expect(filteredDocuments[0].docType).toBe(DOCUMENT_TYPE.EHR); + expect(mockWithParams).toHaveBeenCalledWith(routes.DOCUMENT_UPLOAD); + }); + + it('removes all documents of the specified type', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const removeButton = screen.getByTestId('remove-files-button'); + await user.click(removeButton); + + expect(mockSetDocuments).toHaveBeenCalledTimes(1); + const filteredDocuments = mockSetDocuments.mock.calls[0][0]; + expect(filteredDocuments).toHaveLength(2); + expect( + filteredDocuments.every( + (doc: UploadDocument) => doc.docType === DOCUMENT_TYPE.LLOYD_GEORGE, + ), + ).toBe(true); + }); + + it('prevents default event behavior when clicking remove button', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const removeButton = screen.getByTestId('remove-files-button'); + const clickEvent = vi.fn((e) => e.preventDefault()); + + removeButton.onclick = clickEvent; + await user.click(removeButton); + + expect(clickEvent).toHaveBeenCalled(); + }); + }); + + describe('Cancel button functionality', () => { + it('navigates back when cancel button is clicked', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const cancelButton = screen.getByText('Cancel'); + await user.click(cancelButton); + + expect(mockNavigate).toHaveBeenCalledWith(-1); + }); + + it('does not call setDocuments when cancel is clicked', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const cancelButton = screen.getByText('Cancel'); + await user.click(cancelButton); + + expect(mockSetDocuments).not.toHaveBeenCalled(); + }); }); }); diff --git a/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.tsx b/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.tsx index fb0aef9ee0..75ce3bde7d 100644 --- a/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.tsx +++ b/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.tsx @@ -13,12 +13,14 @@ type Props = { documents: UploadDocument[]; setDocuments: SetUploadDocuments; documentType: DOCUMENT_TYPE; + onSuccess?: () => void; }; const DocumentUploadRemoveFilesStage = ({ documents, setDocuments, documentType, + onSuccess, }: Props): React.JSX.Element => { const navigate = useEnhancedNavigate(); @@ -33,7 +35,12 @@ const DocumentUploadRemoveFilesStage = ({ type="button" id="remove-files-button" data-testid="remove-files-button" - onClick={(): void => { + onClick={(e: React.MouseEvent): void => { + e.preventDefault(); + if (onSuccess) { + onSuccess(); + return; + } setDocuments(documents.filter((doc) => doc.docType !== documentType)); navigate.withParams(routes.DOCUMENT_UPLOAD); }} diff --git a/app/src/pages/adminRoutesPage/AdminRoutesPage.tsx b/app/src/pages/adminRoutesPage/AdminRoutesPage.tsx index 88d2de0df1..29ccb60fd6 100644 --- a/app/src/pages/adminRoutesPage/AdminRoutesPage.tsx +++ b/app/src/pages/adminRoutesPage/AdminRoutesPage.tsx @@ -6,6 +6,7 @@ import ReviewDetailsCompleteStage from '../../components/blocks/_admin/reviewDet import ReviewDetailsDocumentSelectOrderStage from '../../components/blocks/_admin/reviewDetailsDocumentSelectOrderStage/ReviewDetailsDocumentSelectOrderStage'; import ReviewDetailsDocumentSelectStage from '../../components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage'; import ReviewDetailsDocumentUploadingStage from '../../components/blocks/_admin/reviewDetailsDocumentUploadingStage/ReviewDetailsDocumentUploadingStage'; +import ReviewDetailsDocumentRemoveAllStage from '../../components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage'; import ReviewDetailsDontKnowNHSNumberStage from '../../components/blocks/_admin/reviewDetailsDontKnowNHSNumberStage/ReviewDetailsDontKnowNHSNumberStage'; import ReviewDetailsDownloadChoiceStage from '../../components/blocks/_admin/reviewDetailsDownloadChoiceStage/ReviewDetailsDownloadChoiceStage'; import ReviewDetailsFileSelectStage from '../../components/blocks/_admin/reviewDetailsFileSelectStage/ReviewDetailsFileSelectStage'; @@ -162,6 +163,17 @@ const AdminRoutesPage = (): JSX.Element => { /> } /> + {/* routeChildren.REVIEW_DOCUMENT_UPLOAD_REMOVE_ALL */} + + } + /> Date: Wed, 28 Jan 2026 14:02:43 +0000 Subject: [PATCH 2/4] minor fixes --- .../ReviewDetailsDocumentRemoveAllStage.test.tsx | 13 ++----------- .../ReviewDetailsDocumentSelectStage.test.tsx | 5 ----- app/src/pages/adminRoutesPage/AdminRoutesPage.tsx | 1 - 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx index bd5947acae..e5b56d1b60 100644 --- a/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx @@ -15,7 +15,7 @@ const mockNavigate = vi.fn(); const mockReviewId = 'test-review-789'; const mockSetDocuments = vi.fn(); -vi.mock('react-router-dom', async (): Promise => { +vi.mock('react-router-dom', async (): Promise<{}> => { const actual = await vi.importActual('react-router-dom'); return { ...actual, @@ -25,7 +25,7 @@ vi.mock('react-router-dom', async (): Promise => { }); describe('ReviewDetailsDocumentRemoveAllStage', () => { - const testReviewSnoMed: DOCUMENT_TYPE = '16521000000101' as any; + const testReviewSnoMed: DOCUMENT_TYPE = DOCUMENT_TYPE.LLOYD_GEORGE; const mockReviewData: ReviewDetails = new ReviewDetails( mockReviewId, testReviewSnoMed, @@ -59,15 +59,6 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { const mockDocuments: ReviewUploadDocument[] = [mockReviewDocument, mockAdditionalDocument]; - beforeEach(() => { - vi.clearAllMocks(); - import.meta.env.VITE_ENVIRONMENT = 'vitest'; - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - describe('Rendering', () => { it('renders the back link with correct text', () => { render( diff --git a/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.test.tsx b/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.test.tsx index fb2ae5d9d3..2dd9a05904 100644 --- a/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.test.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsDocumentSelectStage/ReviewDetailsDocumentSelectStage.test.tsx @@ -100,11 +100,6 @@ describe('ReviewDetailsDocumentSelectStage', () => { const mockDocuments: UploadDocument[] = []; const mockSetDocuments = vi.fn(); - beforeEach(() => { - vi.clearAllMocks(); - capturedOnSuccessOverride = undefined; - }); - describe('Rendering', () => { it('shows spinner when reviewData is null', () => { render( diff --git a/app/src/pages/adminRoutesPage/AdminRoutesPage.tsx b/app/src/pages/adminRoutesPage/AdminRoutesPage.tsx index 29ccb60fd6..52fbffbff7 100644 --- a/app/src/pages/adminRoutesPage/AdminRoutesPage.tsx +++ b/app/src/pages/adminRoutesPage/AdminRoutesPage.tsx @@ -163,7 +163,6 @@ const AdminRoutesPage = (): JSX.Element => { /> } /> - {/* routeChildren.REVIEW_DOCUMENT_UPLOAD_REMOVE_ALL */} Date: Mon, 2 Feb 2026 11:37:12 +0000 Subject: [PATCH 3/4] code comment changes --- ...viewDetailsDocumentRemoveAllStage.test.tsx | 147 ++------ .../ReviewDetailsDocumentRemoveAllStage.tsx | 11 +- .../ReviewDetailsDocumentSelectStage.test.tsx | 327 ++++++------------ .../DocumentUploadRemoveFilesStage.test.tsx | 24 +- 4 files changed, 130 insertions(+), 379 deletions(-) diff --git a/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx index e5b56d1b60..e7240555ed 100644 --- a/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, RenderResult, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, beforeEach, describe, expect, it, vi, Mock } from 'vitest'; import ReviewDetailsDocumentRemoveAllStage from './ReviewDetailsDocumentRemoveAllStage'; @@ -59,70 +59,39 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { const mockDocuments: ReviewUploadDocument[] = [mockReviewDocument, mockAdditionalDocument]; + const renderApp = ( + reviewData: ReviewDetails | null = mockReviewData, + documents: ReviewUploadDocument[] = mockDocuments, + setDocuments = mockSetDocuments, + ): RenderResult => { + return render( + , + ); + }; + describe('Rendering', () => { - it('renders the back link with correct text', () => { - render( - , - ); + it('renders all expected elements', () => { + renderApp(); expect(screen.getByTestId('back-button')).toBeInTheDocument(); expect(screen.getByText('Go back')).toBeInTheDocument(); - }); - - it('renders DocumentUploadRemoveFilesStage component', () => { - render( - , - ); - expect( screen.getByRole('heading', { name: 'Are you sure you want to remove all selected files?', }), ).toBeInTheDocument(); - }); - - it('renders remove files button', () => { - render( - , - ); - expect( screen.getByRole('button', { name: 'Yes, remove all files' }), ).toBeInTheDocument(); - }); - - it('renders cancel button', () => { - render( - , - ); - expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); }); it('renders spinner when reviewData is null', () => { - render( - , - ); + renderApp(null); expect(screen.getByText('Loading')).toBeInTheDocument(); }); @@ -130,13 +99,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { describe('User Interactions', () => { it('navigates back when back link is clicked', async () => { - render( - , - ); + renderApp(); const backLink = screen.getByTestId('back-button'); await userEvent.click(backLink); @@ -147,13 +110,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { }); it('navigates back when cancel button is clicked', async () => { - render( - , - ); + renderApp(); const cancelButton = screen.getByRole('button', { name: 'Cancel' }); await userEvent.click(cancelButton); @@ -164,13 +121,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { }); it('filters out additional documents when remove all button is clicked', async () => { - render( - , - ); + renderApp(); const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); await userEvent.click(removeButton); @@ -183,13 +134,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { }); it('keeps only REVIEW type documents after removal', async () => { - render( - , - ); + renderApp(); const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); await userEvent.click(removeButton); @@ -202,13 +147,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { }); it('calls setDocuments and navigates after removal', async () => { - render( - , - ); + renderApp(); const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); await userEvent.click(removeButton); @@ -219,13 +158,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { }); it('handles empty documents array', async () => { - render( - , - ); + renderApp(mockReviewData, []); const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); await userEvent.click(removeButton); @@ -238,13 +171,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { it('handles documents with only REVIEW type', async () => { const reviewOnlyDocs: ReviewUploadDocument[] = [mockReviewDocument]; - render( - , - ); + renderApp(mockReviewData, reviewOnlyDocs); const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); await userEvent.click(removeButton); @@ -257,13 +184,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { it('handles documents with only additional files', async () => { const additionalOnlyDocs: ReviewUploadDocument[] = [mockAdditionalDocument]; - render( - , - ); + renderApp(mockReviewData, additionalOnlyDocs); const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); await userEvent.click(removeButton); @@ -289,13 +210,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { }, ]; - render( - , - ); + renderApp(mockReviewData, multipleAdditionalDocs); const removeButton = screen.getByRole('button', { name: 'Yes, remove all files' }); await userEvent.click(removeButton); @@ -310,13 +225,7 @@ describe('ReviewDetailsDocumentRemoveAllStage', () => { describe('Accessibility', () => { it('passes accessibility checks', async () => { - const { container } = render( - , - ); + const { container } = renderApp(); const results = await runAxeTest(container); expect(results).toHaveNoViolations(); diff --git a/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.tsx b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.tsx index f21d3ad1ab..2da1b7888b 100644 --- a/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsDocumentRemoveAllStage/ReviewDetailsDocumentRemoveAllStage.tsx @@ -8,7 +8,7 @@ import { } from '../../../../types/pages/UploadDocumentsPage/types'; import Spinner from '../../../generic/spinner/Spinner'; import DocumentUploadRemoveFilesStage from '../../_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage'; -import { BackLink } from 'nhsuk-react-components'; +import BackButton from '../../../generic/backButton/BackButton'; type ReviewDetailsDocumentRemoveAllStageProps = { reviewData: ReviewDetails | null; @@ -38,16 +38,9 @@ const ReviewDetailsDocumentRemoveAllStage = ({ ); }; - const handleGoBack = (e: React.MouseEvent): void => { - e.preventDefault(); - navigate(-1); - }; - return ( <> - - Go back - + void) | undefined; - -vi.mock('../../_documentUpload/documentSelectStage/DocumentSelectStage', () => ({ - default: vi.fn( - ({ - documents, - setDocuments, - documentType, - filesErrorRef, - onSuccessOverride, - backLinkOverride, - removeAllFilesLinkOverride, - isReview, - }) => { - capturedOnSuccessOverride = onSuccessOverride; - return ( -
-
{documents.length}
-
{documentType}
-
{backLinkOverride}
-
- {removeAllFilesLinkOverride} -
-
{isReview ? 'true' : 'false'}
- - -
- {filesErrorRef.current !== undefined ? 'has ref' : 'no ref'} -
-
- ); + +// Mock pdfjs-dist to avoid DOMMatrix issues in test environment +vi.mock('pdfjs-dist', () => ({ + getDocument: vi.fn(() => ({ + promise: Promise.resolve({ + getPage: vi.fn(() => Promise.resolve({})), + numPages: 1, + destroy: vi.fn(() => Promise.resolve()), + }), + })), +})); + +// Mock PatientSummary component as it's not relevant for these tests +vi.mock('../../../generic/patientSummary/PatientSummary', () => ({ + default: Object.assign( + vi.fn(() => null), + { + Child: vi.fn(() => null), }, ), + PatientInfo: { + FULL_NAME: 'fullName', + NHS_NUMBER: 'nhsNumber', + BIRTH_DATE: 'birthDate', + }, })); +// Mock Spinner component vi.mock('../../../generic/spinner/Spinner', () => ({ default: vi.fn(() =>
Loading...
), })); @@ -83,22 +49,30 @@ vi.mock('react-router-dom', () => ({ })); describe('ReviewDetailsDocumentSelectStage', () => { - const testReviewSnoMed: DOCUMENT_TYPE = '16521000000101' as DOCUMENT_TYPE; - const mockReviewData: ReviewDetails = new ReviewDetails( - 'test-review-id', - testReviewSnoMed, - '2024-01-01T12:00:00Z', - 'Test Uploader', - '2024-01-01T12:00:00Z', - 'Test Reason', - '1', - '1234567890', - ); - - mockReviewData.files = []; - - const mockDocuments: UploadDocument[] = []; - const mockSetDocuments = vi.fn(); + const testReviewSnoMed: DOCUMENT_TYPE = DOCUMENT_TYPE.LLOYD_GEORGE; + + let mockReviewData: ReviewDetails; + let mockDocuments: UploadDocument[]; + let mockSetDocuments: SetUploadDocuments; + + beforeEach(() => { + vi.clearAllMocks(); + + mockReviewData = new ReviewDetails( + 'test-review-id', + testReviewSnoMed, + '2024-01-01T12:00:00Z', + 'Test Uploader', + '2024-01-01T12:00:00Z', + 'Test Reason', + '1', + '1234567890', + ); + mockReviewData.files = []; + + mockDocuments = []; + mockSetDocuments = vi.fn() as SetUploadDocuments; + }); describe('Rendering', () => { it('shows spinner when reviewData is null', () => { @@ -138,8 +112,8 @@ describe('ReviewDetailsDocumentSelectStage', () => { }); }); - describe('Props handling', () => { - it('passes correct props to DocumentSelectStage', async () => { + describe('Integration with DocumentSelectStage', () => { + it('renders the DocumentSelectStage component when documents are initialized', async () => { const testDocuments: UploadDocument[] = [ { id: 'test-id', @@ -170,16 +144,23 @@ describe('ReviewDetailsDocumentSelectStage', () => { ); await waitFor(() => { - expect(screen.getByTestId('document-type')).toBeInTheDocument(); + expect(screen.queryByTestId('mock-spinner')).not.toBeInTheDocument(); }); - expect(screen.getByTestId('document-type')).toHaveTextContent(testReviewSnoMed); + + // Check that the actual DocumentSelectStage component is rendered + // by looking for the page title + expect( + screen.getByText('Choose scanned paper notes files to upload'), + ).toBeInTheDocument(); }); - it('passes documents array to DocumentSelectStage', async () => { + it('displays document information correctly', async () => { const testDocuments: UploadDocument[] = [ { id: 'test-id', - file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), + file: new File(['test content'], 'test-document.pdf', { + type: 'application/pdf', + }), state: DOCUMENT_UPLOAD_STATE.SELECTED, progress: 0, docType: testReviewSnoMed, @@ -206,14 +187,15 @@ describe('ReviewDetailsDocumentSelectStage', () => { ); await waitFor(() => { - expect(screen.getByTestId('documents-length')).toBeInTheDocument(); + expect(screen.queryByTestId('mock-spinner')).not.toBeInTheDocument(); }); - expect(screen.getByTestId('documents-length')).toHaveTextContent('1'); + + // The file name should be displayed in the document table + expect(screen.getByText('test-document.pdf')).toBeInTheDocument(); }); - }); - describe('onSuccess callback', () => { - it('navigates to upload file order page when onSuccess is called', async () => { + it('provides correct back link based on review ID', async () => { + const user = userEvent.setup(); const testDocuments: UploadDocument[] = [ { id: 'test-id', @@ -244,23 +226,24 @@ describe('ReviewDetailsDocumentSelectStage', () => { ); await waitFor(() => { - expect(screen.getByTestId('trigger-success')).toBeInTheDocument(); + expect(screen.queryByTestId('mock-spinner')).not.toBeInTheDocument(); }); - const triggerButton = screen.getByTestId('trigger-success'); - triggerButton.click(); + const expectedBackLink = routeChildren.ADMIN_REVIEW_ADD_MORE_CHOICE.replaceAll( + ':reviewId', + 'test-review-id.1', + ); - await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith( - routeChildren.ADMIN_REVIEW_UPLOAD_FILE_ORDER.replaceAll( - ':reviewId', - 'test-review-id.1', - ), - ); - }); + const backButton = screen.getByTestId('back-button'); + await user.click(backButton); + + expect(mockNavigate).toHaveBeenCalledWith(expectedBackLink); }); + }); - it('passes onSuccessOverride to DocumentSelectStage', async () => { + describe('Continue button navigation', () => { + it('navigates to upload file order page when continue is clicked with documents', async () => { + const user = userEvent.setup(); const testDocuments: UploadDocument[] = [ { id: 'test-id', @@ -291,13 +274,24 @@ describe('ReviewDetailsDocumentSelectStage', () => { ); await waitFor(() => { - expect(capturedOnSuccessOverride).toBeDefined(); + expect(screen.queryByTestId('mock-spinner')).not.toBeInTheDocument(); }); - expect(typeof capturedOnSuccessOverride).toBe('function'); + const continueButton = screen.getByRole('button', { name: /continue/i }); + await user.click(continueButton); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith( + routeChildren.ADMIN_REVIEW_UPLOAD_FILE_ORDER.replaceAll( + ':reviewId', + 'test-review-id.1', + ), + ); + }); }); - it('constructs correct reviewId from review data', async () => { + it('constructs correct reviewId with different version number', async () => { + const user = userEvent.setup(); const customReviewData = new ReviewDetails( 'custom-id', testReviewSnoMed, @@ -340,138 +334,15 @@ describe('ReviewDetailsDocumentSelectStage', () => { ); await waitFor(() => { - expect(screen.getByTestId('trigger-success')).toBeInTheDocument(); + expect(screen.queryByTestId('mock-spinner')).not.toBeInTheDocument(); }); - const triggerButton = screen.getByTestId('trigger-success'); - triggerButton.click(); + const continueButton = screen.getByRole('button', { name: /continue/i }); + await user.click(continueButton); await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith(expect.stringContaining('custom-id.5')); }); }); }); - - describe('Prop overrides', () => { - it('passes backLinkOverride with correct reviewId', async () => { - const testDocuments: UploadDocument[] = [ - { - id: 'test-id', - file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), - state: DOCUMENT_UPLOAD_STATE.SELECTED, - progress: 0, - docType: testReviewSnoMed, - attempts: 0, - numPages: 1, - validated: false, - }, - ]; - - const { rerender } = render( - , - ); - - rerender( - , - ); - - await waitFor(() => { - expect(screen.getByTestId('back-link-override')).toBeInTheDocument(); - }); - - const expectedBackLink = routeChildren.ADMIN_REVIEW_ADD_MORE_CHOICE.replaceAll( - ':reviewId', - 'test-review-id.1', - ); - expect(screen.getByTestId('back-link-override')).toHaveTextContent(expectedBackLink); - }); - - it('passes removeAllFilesLinkOverride with correct reviewId', async () => { - const testDocuments: UploadDocument[] = [ - { - id: 'test-id', - file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), - state: DOCUMENT_UPLOAD_STATE.SELECTED, - progress: 0, - docType: testReviewSnoMed, - attempts: 0, - numPages: 1, - validated: false, - }, - ]; - - const { rerender } = render( - , - ); - - rerender( - , - ); - - await waitFor(() => { - expect(screen.getByTestId('remove-all-files-link-override')).toBeInTheDocument(); - }); - - const expectedRemoveAllLink = routeChildren.ADMIN_REVIEW_REMOVE_ALL.replaceAll( - ':reviewId', - 'test-review-id.1', - ); - expect(screen.getByTestId('remove-all-files-link-override')).toHaveTextContent( - expectedRemoveAllLink, - ); - }); - - it('passes isReview as true', async () => { - const testDocuments: UploadDocument[] = [ - { - id: 'test-id', - file: new File(['test'], 'test.pdf', { type: 'application/pdf' }), - state: DOCUMENT_UPLOAD_STATE.SELECTED, - progress: 0, - docType: testReviewSnoMed, - attempts: 0, - numPages: 1, - validated: false, - }, - ]; - - const { rerender } = render( - , - ); - - rerender( - , - ); - - await waitFor(() => { - expect(screen.getByTestId('is-review')).toBeInTheDocument(); - }); - - expect(screen.getByTestId('is-review')).toHaveTextContent('true'); - }); - }); }); diff --git a/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.test.tsx b/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.test.tsx index 722d173e6e..190db47ae4 100644 --- a/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.test.tsx +++ b/app/src/components/blocks/_documentUpload/documentUploadRemoveFilesStage/DocumentUploadRemoveFilesStage.test.tsx @@ -60,7 +60,7 @@ describe('DocumentUploadRemoveFilesStage', () => { }); describe('Rendering', () => { - it('renders the confirmation page with correct title', async () => { + it('renders the confirmation page with all elements', async () => { render( { screen.getByText('Are you sure you want to remove all selected files?'), ).toBeInTheDocument(); }); - }); - - it('renders remove files button', () => { - render( - , - ); - expect(screen.getByTestId('remove-files-button')).toBeInTheDocument(); expect(screen.getByText('Yes, remove all files')).toBeInTheDocument(); - }); - - it('renders cancel button', () => { - render( - , - ); - expect(screen.getByText('Cancel')).toBeInTheDocument(); }); }); From d2ee96023de0f5a6e7121df0ec0ae7b3b288cb33 Mon Sep 17 00:00:00 2001 From: Lillie Dae Date: Mon, 2 Feb 2026 13:38:55 +0000 Subject: [PATCH 4/4] improve test coverage --- .../ReviewDetailsFileSelectStage.test.tsx | 361 +++++++++--------- 1 file changed, 186 insertions(+), 175 deletions(-) diff --git a/app/src/components/blocks/_admin/reviewDetailsFileSelectStage/ReviewDetailsFileSelectStage.test.tsx b/app/src/components/blocks/_admin/reviewDetailsFileSelectStage/ReviewDetailsFileSelectStage.test.tsx index 527931aa61..837350d68c 100644 --- a/app/src/components/blocks/_admin/reviewDetailsFileSelectStage/ReviewDetailsFileSelectStage.test.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsFileSelectStage/ReviewDetailsFileSelectStage.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, RenderResult, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { useState } from 'react'; import { beforeEach, describe, expect, it, vi, Mock } from 'vitest'; @@ -64,7 +64,7 @@ const renderApp = ({ uploadDocuments?: ReviewUploadDocument[]; setUploadDocuments?: React.Dispatch>; mockPatientContext?: boolean; -} = {}) => { +} = {}): RenderResult => { if (mockPatientContext) { mockUsePatientDetailsContext.mockReturnValue([null, mockSetPatientDetails]); } @@ -107,7 +107,15 @@ describe('ReviewDetailsFileSelectStage', () => { }); it('renders a spinner when reviewData is null', () => { - renderApp(); + mockUsePatientDetailsContext.mockReturnValue([null, mockSetPatientDetails]); + + render( + , + ); expect(screen.getByLabelText('Loading')).toBeInTheDocument(); }); @@ -122,7 +130,13 @@ describe('ReviewDetailsFileSelectStage', () => { ), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); + render( + , + ); expect( screen.getByRole('heading', { @@ -161,7 +175,13 @@ describe('ReviewDetailsFileSelectStage', () => { makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); + render( + , + ); await user.click(screen.getByRole('button', { name: /View file1\.pdf/i })); @@ -176,7 +196,13 @@ describe('ReviewDetailsFileSelectStage', () => { makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); + render( + , + ); await user.click(screen.getByRole('button', { name: 'Continue' })); @@ -208,7 +234,6 @@ describe('ReviewDetailsFileSelectStage', () => { ); }; - mockUsePatientDetailsContext.mockReturnValue([null, mockSetPatientDetails]); render(); await user.click(screen.getByRole('button', { name: 'Continue' })); @@ -228,7 +253,13 @@ describe('ReviewDetailsFileSelectStage', () => { makeReviewDoc('file2.pdf', DOCUMENT_UPLOAD_STATE.SELECTED, UploadDocumentType.REVIEW), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: initialDocs }); + render( + , + ); await user.click(screen.getByRole('button', { name: 'Continue' })); @@ -245,7 +276,13 @@ describe('ReviewDetailsFileSelectStage', () => { makeReviewDoc('file2.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: initialDocs }); + render( + , + ); await user.click(screen.getByRole('button', { name: 'Continue' })); expect(mockNavigate).toHaveBeenCalledWith( @@ -254,31 +291,28 @@ describe('ReviewDetailsFileSelectStage', () => { }); it('does not navigate if reviewId is missing', async () => { - const user = userEvent.setup(); currentReviewId = undefined; const documents: ReviewUploadDocument[] = [ makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.SELECTED, UploadDocumentType.REVIEW), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); + render( + , + ); - await user.click(screen.getByRole('button', { name: 'Continue' })); - expect(mockNavigate).not.toHaveBeenCalled(); + expect(screen.getByRole('checkbox', { name: /Select file1\.pdf/i })).toBeChecked(); }); - it('renders back button', () => { - const documents: ReviewUploadDocument[] = [ - makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), - ]; - - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); - - expect(screen.getByTestId('back-button')).toBeInTheDocument(); - expect(screen.getByText('Go back')).toBeInTheDocument(); - }); + it('renders correct document type display name in heading', () => { + mockGetConfigForDocType.mockReturnValue({ + displayName: 'test document type', + }); - it('renders continue section with guidance text', () => { const documents: ReviewUploadDocument[] = [ makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), ]; @@ -286,29 +320,57 @@ describe('ReviewDetailsFileSelectStage', () => { renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); expect( - screen.getByText('If you need to add any more files you can do this next.'), + screen.getByRole('heading', { + name: /Choose files to add to the existing Test document type/i, + }), ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); }); - it('renders table headers correctly', () => { + it('only filters and displays REVIEW type documents in table', () => { const documents: ReviewUploadDocument[] = [ - makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), + makeReviewDoc( + 'review1.pdf', + DOCUMENT_UPLOAD_STATE.UNSELECTED, + UploadDocumentType.REVIEW, + ), + makeReviewDoc( + 'review2.pdf', + DOCUMENT_UPLOAD_STATE.UNSELECTED, + UploadDocumentType.REVIEW, + ), + makeReviewDoc( + 'existing1.pdf', + DOCUMENT_UPLOAD_STATE.UNSELECTED, + UploadDocumentType.EXISTING, + ), + makeReviewDoc( + 'existing2.pdf', + DOCUMENT_UPLOAD_STATE.UNSELECTED, + UploadDocumentType.EXISTING, + ), ]; renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); - expect(screen.getByRole('columnheader', { name: 'Filename' })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: 'Date received' })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: 'View file' })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: 'Select' })).toBeInTheDocument(); + expect(screen.getByText('review1.pdf')).toBeInTheDocument(); + expect(screen.getByText('review2.pdf')).toBeInTheDocument(); + expect(screen.queryByText('existing1.pdf')).not.toBeInTheDocument(); + expect(screen.queryByText('existing2.pdf')).not.toBeInTheDocument(); }); - it('allows multiple files to be selected', async () => { + it('only updates REVIEW type documents when selecting files', async () => { const user = userEvent.setup(); const initialDocs: ReviewUploadDocument[] = [ - makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), - makeReviewDoc('file2.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), + makeReviewDoc( + 'review1.pdf', + DOCUMENT_UPLOAD_STATE.UNSELECTED, + UploadDocumentType.REVIEW, + ), + makeReviewDoc( + 'existing1.pdf', + DOCUMENT_UPLOAD_STATE.UNSELECTED, + UploadDocumentType.EXISTING, + ), ]; const Wrapper = (): React.JSX.Element => { @@ -322,23 +384,26 @@ describe('ReviewDetailsFileSelectStage', () => { ); }; - mockUsePatientDetailsContext.mockReturnValue([null, mockSetPatientDetails]); render(); - const checkbox1 = screen.getByRole('checkbox', { name: /Select file1\.pdf/i }); - const checkbox2 = screen.getByRole('checkbox', { name: /Select file2\.pdf/i }); + const checkbox = screen.getByRole('checkbox', { name: /Select review1\.pdf/i }); + await user.click(checkbox); - await user.click(checkbox1); - await user.click(checkbox2); + expect(checkbox).toBeChecked(); - expect(checkbox1).toBeChecked(); - expect(checkbox2).toBeChecked(); + // Verify only REVIEW documents are shown in table (EXISTING should not be visible) + expect(screen.queryByText('existing1.pdf')).not.toBeInTheDocument(); }); - it('allows unselecting a previously selected file', async () => { + it('only updates REVIEW type documents when unselecting files', async () => { const user = userEvent.setup(); const initialDocs: ReviewUploadDocument[] = [ - makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.SELECTED, UploadDocumentType.REVIEW), + makeReviewDoc('review1.pdf', DOCUMENT_UPLOAD_STATE.SELECTED, UploadDocumentType.REVIEW), + makeReviewDoc( + 'existing1.pdf', + DOCUMENT_UPLOAD_STATE.SELECTED, + UploadDocumentType.EXISTING, + ), ]; const Wrapper = (): React.JSX.Element => { @@ -352,186 +417,132 @@ describe('ReviewDetailsFileSelectStage', () => { ); }; - mockUsePatientDetailsContext.mockReturnValue([null, mockSetPatientDetails]); render(); - const checkbox = screen.getByRole('checkbox', { name: /Select file1\.pdf/i }); - expect(checkbox).toBeChecked(); - + const checkbox = screen.getByRole('checkbox', { name: /Select review1\.pdf/i }); await user.click(checkbox); + expect(checkbox).not.toBeChecked(); }); - it('switches between viewing different files', async () => { - const user = userEvent.setup(); + it('displays upload date for each file in the table', () => { const documents: ReviewUploadDocument[] = [ makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), makeReviewDoc('file2.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); - - await user.click(screen.getByRole('button', { name: /View file1\.pdf/i })); - expect(screen.getByText(/You are currently viewing: file1.pdf/i)).toBeInTheDocument(); - - await user.click(screen.getByRole('button', { name: /View file2\.pdf/i })); - expect(screen.getByText(/You are currently viewing: file2.pdf/i)).toBeInTheDocument(); + renderApp({ + reviewData: mockReviewData, + uploadDocuments: documents, + }); - expect(globalThis.URL.createObjectURL).toHaveBeenCalledTimes(2); + expect(screen.getByText('file1.pdf')).toBeInTheDocument(); + expect(screen.getByText('file2.pdf')).toBeInTheDocument(); + + const dates = screen.getAllByText('21 January 1970'); + expect(dates).toHaveLength(2); }); - it('displays formatted upload dates correctly', () => { - const mockReviewDataWithDates = { - snomedCode: testReviewSnoMed, - files: [ - { - fileName: 'file1.pdf', - uploadDate: 1609459200000, // 1 January 2021 in milliseconds - }, - { - fileName: 'file2.pdf', - uploadDate: 1612137600000, // 1 February 2021 in milliseconds - }, - ], - } as any; - + it('does not create object URL when viewing a non-existent file', async () => { + const user = userEvent.setup(); const documents: ReviewUploadDocument[] = [ makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), - makeReviewDoc('file2.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), ]; - renderApp({ reviewData: mockReviewDataWithDates, uploadDocuments: documents }); - - expect(screen.getByText('1 January 2021')).toBeInTheDocument(); - expect(screen.getByText('1 February 2021')).toBeInTheDocument(); - }); + const mockSetUploadDocuments = vi.fn(); - it('renders new files section heading', () => { - const documents: ReviewUploadDocument[] = [ - makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), - ]; + renderApp({ + reviewData: mockReviewData, + uploadDocuments: documents, + setUploadDocuments: mockSetUploadDocuments, + }); - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); + await user.click(screen.getByRole('button', { name: /View file1\.pdf/i })); - expect(screen.getByRole('heading', { name: 'New files' })).toBeInTheDocument(); + // PDF viewer should be shown with the object URL + expect(globalThis.URL.createObjectURL).toHaveBeenCalledTimes(1); + expect(screen.getByText(/You are currently viewing: file1\.pdf/i)).toBeInTheDocument(); }); - it('shows error message in fieldset when no files are selected', async () => { + it('only counts REVIEW type documents when checking for selected files', async () => { const user = userEvent.setup(); const documents: ReviewUploadDocument[] = [ - makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), + makeReviewDoc( + 'review1.pdf', + DOCUMENT_UPLOAD_STATE.UNSELECTED, + UploadDocumentType.REVIEW, + ), + makeReviewDoc( + 'existing1.pdf', + DOCUMENT_UPLOAD_STATE.SELECTED, + UploadDocumentType.EXISTING, + ), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); + renderApp({ + reviewData: mockReviewData, + uploadDocuments: documents, + }); + // Even though existing1.pdf is SELECTED, it should show error + // because no REVIEW documents are selected await user.click(screen.getByRole('button', { name: 'Continue' })); - expect(screen.getByText('Select at least one file')).toBeInTheDocument(); - }); - - it('does not render PDF viewer when no file is selected', () => { - const documents: ReviewUploadDocument[] = [ - makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), - ]; - - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); - - expect(screen.queryByTestId('pdf-viewer')).not.toBeInTheDocument(); - expect(screen.queryByText(/You are currently viewing:/i)).not.toBeInTheDocument(); + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByText('You need to select an option')).toBeInTheDocument(); }); - it('calls setUploadDocuments when a file is selected', async () => { + it('only counts REVIEW type documents when checking for unselected files', async () => { const user = userEvent.setup(); - const mockSetDocs = vi.fn(); const documents: ReviewUploadDocument[] = [ - makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), + makeReviewDoc('review1.pdf', DOCUMENT_UPLOAD_STATE.SELECTED, UploadDocumentType.REVIEW), + makeReviewDoc( + 'existing1.pdf', + DOCUMENT_UPLOAD_STATE.UNSELECTED, + UploadDocumentType.EXISTING, + ), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: documents, setUploadDocuments: mockSetDocs }); - - const checkbox = screen.getByRole('checkbox', { name: /Select file1\.pdf/i }); - - await user.click(checkbox); - - expect(mockSetDocs).toHaveBeenCalled(); - }); - - it('maintains checkbox state across re-renders', () => { - const selectedDoc = makeReviewDoc( - 'selected.pdf', - DOCUMENT_UPLOAD_STATE.SELECTED, - UploadDocumentType.REVIEW, - ); - const unselectedDoc = makeReviewDoc( - 'unselected.pdf', - DOCUMENT_UPLOAD_STATE.UNSELECTED, - UploadDocumentType.REVIEW, - ); - - const { rerender } = renderApp({ + renderApp({ reviewData: mockReviewData, - uploadDocuments: [selectedDoc, unselectedDoc], + uploadDocuments: documents, }); - expect(screen.getByRole('checkbox', { name: /Select selected\.pdf/i })).toBeChecked(); - expect( - screen.getByRole('checkbox', { name: /Select unselected\.pdf/i }), - ).not.toBeChecked(); + // All REVIEW documents are selected, should navigate to add-more-choice + // even though existing1.pdf is UNSELECTED + await user.click(screen.getByRole('button', { name: 'Continue' })); - mockUsePatientDetailsContext.mockReturnValue([null, mockSetPatientDetails]); - rerender( - , + expect(mockNavigate).toHaveBeenCalledWith( + routeChildren.ADMIN_REVIEW_ADD_MORE_CHOICE.replace(':reviewId', mockReviewId), + undefined, ); - - expect(screen.getByRole('checkbox', { name: /Select selected\.pdf/i })).toBeChecked(); - expect( - screen.getByRole('checkbox', { name: /Select unselected\.pdf/i }), - ).not.toBeChecked(); }); - it('renders correct document type display name in heading', () => { - mockGetConfigForDocType.mockReturnValue({ - displayName: 'test document type', - }); - - const documents: ReviewUploadDocument[] = [ + it('handles file selection when document is not found', async () => { + const user = userEvent.setup(); + const initialDocs: ReviewUploadDocument[] = [ makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); + const mockSetUploadDocuments = vi.fn(); + const setDocs = vi.fn(); - expect( - screen.getByRole('heading', { - name: /Choose files to add to the existing Test document type/i, - }), - ).toBeInTheDocument(); - }); + const customSetter = (updater: React.SetStateAction): void => { + mockSetUploadDocuments(updater); + // Simulate document not being found by clearing the array + setDocs([]); + }; - it('only filters and displays REVIEW type documents in table', () => { - const documents: ReviewUploadDocument[] = [ - makeReviewDoc('review1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), - makeReviewDoc('review2.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), - makeReviewDoc( - 'existing1.pdf', - DOCUMENT_UPLOAD_STATE.UNSELECTED, - UploadDocumentType.EXISTING, - ), - makeReviewDoc( - 'existing2.pdf', - DOCUMENT_UPLOAD_STATE.UNSELECTED, - UploadDocumentType.EXISTING, - ), - ]; + renderApp({ + reviewData: mockReviewData, + uploadDocuments: initialDocs, + setUploadDocuments: customSetter, + }); - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); + const checkbox = screen.getByRole('checkbox', { name: /Select file1\.pdf/i }); + await user.click(checkbox); - expect(screen.getByText('review1.pdf')).toBeInTheDocument(); - expect(screen.getByText('review2.pdf')).toBeInTheDocument(); - expect(screen.queryByText('existing1.pdf')).not.toBeInTheDocument(); - expect(screen.queryByText('existing2.pdf')).not.toBeInTheDocument(); + // After clicking, the document won't be found in the next render + expect(mockSetUploadDocuments).toHaveBeenCalled(); }); });