diff --git a/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_general_browser_states.cy.js b/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_general_browser_states.cy.js index fc96f2d33..886c43cb4 100644 --- a/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_general_browser_states.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_general_browser_states.cy.js @@ -50,7 +50,7 @@ describe('Authentication & Authorisation', () => { routes.home, routes.patientSearch, '/patient/verify', - '/patient/arf', + '/patient/documents', '/patient/lloyd-george-record', routes.createReport, routes.createReportComplete, diff --git a/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_gp_admin_path_access.cy.js b/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_gp_admin_path_access.cy.js index 129c87d0c..148bf7185 100644 --- a/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_gp_admin_path_access.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_gp_admin_path_access.cy.js @@ -17,9 +17,6 @@ const patient = { const baseUrl = Cypress.config('baseUrl'); const patientVerifyUrl = '/patient/verify'; const lloydGeorgeViewUrl = '/patient/lloyd-george-record'; -const arfDownloadUrl = '/patient/arf'; - -const forbiddenRoutes = [arfDownloadUrl]; describe('GP Admin user role has access to the expected GP_ADMIN workflow paths', () => { context('GP Admin role has access to expected routes', () => { @@ -52,19 +49,3 @@ describe('GP Admin user role has access to the expected GP_ADMIN workflow paths' }); }); }); - -describe('GP Admin user role cannot access expected forbidden routes', () => { - context('GP Admin role has no access to forbidden routes', () => { - forbiddenRoutes.forEach((forbiddenRoute) => { - it( - 'GP Admin role cannot access route ' + forbiddenRoute, - { tags: 'regression' }, - () => { - cy.login(Roles.GP_ADMIN); - cy.visit(forbiddenRoute); - cy.url().should('include', 'unauthorised'); - }, - ); - }); - }); -}); diff --git a/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_gp_clinical_path_access.cy.js b/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_gp_clinical_path_access.cy.js index 59ece2a83..9135d762a 100644 --- a/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_gp_clinical_path_access.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_gp_clinical_path_access.cy.js @@ -17,8 +17,6 @@ const patient = { const baseUrl = Cypress.config('baseUrl'); const patientVerifyUrl = '/patient/verify'; const lloydGeorgeViewUrl = '/patient/lloyd-george-record'; -const arfDownloadUrl = '/patient/arf'; -const forbiddenRoutes = [arfDownloadUrl]; describe('GP Clinical user role has access to the expected GP_CLINICAL workflow paths', () => { context(`GP Clinical role has access to expected routes`, () => { @@ -50,20 +48,4 @@ describe('GP Clinical user role has access to the expected GP_CLINICAL workflow cy.url().should('eq', baseUrl + lloydGeorgeViewUrl); }); }); -}); - -describe('GP Clinical user role cannot access expected forbidden routes', () => { - context('GP Clinical role has no access to forbidden routes', () => { - forbiddenRoutes.forEach((forbiddenRoute) => { - it( - 'GP Clinical role cannot access route ' + forbiddenRoute, - { tags: 'regression' }, - () => { - cy.login(Roles.GP_CLINICAL); - cy.visit(forbiddenRoute); - cy.url().should('include', 'unauthorised'); - }, - ); - }); - }); -}); +}); \ No newline at end of file diff --git a/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_pcse_path_access.cy.js b/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_pcse_path_access.cy.js index 9961c1541..e0b3c4454 100644 --- a/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_pcse_path_access.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/auth_routes/auth_pcse_path_access.cy.js @@ -1,5 +1,6 @@ import { Roles } from '../../../support/roles'; import { routes } from '../../../support/routes'; +import { DOCUMENT_TYPE } from '../../../../src/helpers/utils/documentType'; const testPatient = '9000000009'; const patient = { @@ -16,7 +17,7 @@ const patient = { const baseUrl = Cypress.config('baseUrl'); const lloydGeorgeViewUrl = '/patient/lloyd-george-record'; -const arfDownloadUrl = '/patient/arf'; +const documentsUrl = '/patient/documents'; const forbiddenRoutes = [lloydGeorgeViewUrl]; @@ -28,6 +29,11 @@ describe('PCSE user role has access to the expected GP_ADMIN workflow paths', () body: patient, }).as('search'); + cy.intercept('GET', '/SearchDocumentReferences*', { + statusCode: 200, + body: [], + }).as('documentSearch'); + cy.login(Roles.PCSE); cy.url().should('eq', baseUrl + routes.home); @@ -40,7 +46,9 @@ describe('PCSE user role has access to the expected GP_ADMIN workflow paths', () cy.wait('@search'); cy.get('#verify-submit').click(); - cy.url().should('eq', baseUrl + arfDownloadUrl); + + cy.wait('@documentSearch'); + cy.url().should('eq', baseUrl + documentsUrl); }); }); }); diff --git a/app/cypress/e2e/0-ndr-core-tests/feature_flag_workflows/arf_workflow.cy.js b/app/cypress/e2e/0-ndr-core-tests/feature_flag_workflows/arf_workflow.cy.js deleted file mode 100644 index 0918bdea0..000000000 --- a/app/cypress/e2e/0-ndr-core-tests/feature_flag_workflows/arf_workflow.cy.js +++ /dev/null @@ -1,122 +0,0 @@ -import { Roles, roleName } from '../../../support/roles'; -import { routes } from '../../../support/routes'; - -const baseUrl = Cypress.config('baseUrl'); - -const arfUploadUrl = '/patient/arf/upload'; -const unauthorisedUrl = '/unauthorised'; - -const testPatient = '9000000009'; -const patient = { - birthDate: '1970-01-01', - familyName: 'Default Surname', - givenName: ['Default Given Name'], - nhsNumber: testPatient, - postalCode: 'AA1 1AA', - superseded: false, - restricted: false, - active: false, - deceased: false, -}; - -const navigateToUploadPage = () => { - cy.intercept('GET', '/SearchPatient*', { - statusCode: 200, - body: patient, - }).as('search'); - - cy.visit(routes.patientSearch); - cy.get('#nhs-number-input').click(); - cy.get('#nhs-number-input').type(testPatient); - - cy.get('#search-submit').click(); - cy.wait('@search'); -}; - -const gpRoles = [Roles.GP_ADMIN, Roles.GP_CLINICAL]; - -describe('Feature flags - ARF Workflow', () => { - it( - 'for GP clinical role it does not find patient when both feature flags are enabled', - { tags: 'regression' }, - () => { - cy.login(Roles.GP_CLINICAL); - navigateToUploadPage(); - cy.get('#nhs-number-input--error-message').should('be.visible'); - cy.get('#nhs-number-input--error-message').should( - 'include.text', - "Error: You cannot access this patient's record", - ); - cy.get('#error-box-summary').should('be.visible'); - cy.get('#error-box-summary').should('have.text', 'There is a problem'); - }, - ); - gpRoles.forEach((role) => { - context(`As a ${roleName(role)} user visiting the ARF page for an inactive patient`, () => { - it( - 'displays the error when ARF workflow feature flag is disabled', - { tags: 'regression' }, - () => { - const featureFlags = { - uploadArfWorkflowEnabled: false, - uploadLambdaEnabled: true, - }; - cy.login(role, featureFlags); - navigateToUploadPage(); - - cy.get('#nhs-number-input--error-message').should('be.visible'); - cy.get('#nhs-number-input--error-message').should( - 'include.text', - "Error: You cannot access this patient's record", - ); - cy.get('#error-box-summary').should('be.visible'); - cy.get('#error-box-summary').should('have.text', 'There is a problem'); - }, - ); - - it( - 'displays the error page when upload lambda feature flag is disabled', - { tags: 'regression' }, - () => { - const featureFlags = { - uploadArfWorkflowEnabled: true, - uploadLambdaEnabled: false, - }; - - cy.login(role, featureFlags); - navigateToUploadPage(); - - cy.get('#nhs-number-input--error-message').should('be.visible'); - cy.get('#nhs-number-input--error-message').should( - 'include.text', - "Error: You cannot access this patient's record", - ); - cy.get('#error-box-summary').should('be.visible'); - cy.get('#error-box-summary').should('have.text', 'There is a problem'); - }, - ); - - it( - 'displays the error page when both upload and ARF workflow feature flag are disabled', - { tags: 'regression' }, - () => { - const featureFlags = { - uploadArfWorkflowEnabled: false, - uploadLambdaEnabled: false, - }; - - cy.login(role, featureFlags); - navigateToUploadPage(); - - cy.get('#nhs-number-input--error-message').should('be.visible'); - cy.get('#nhs-number-input--error-message').should( - 'include.text', - "Error: You cannot access this patient's record", - ); - cy.get('#error-box-summary').should('be.visible'); - cy.get('#error-box-summary').should('have.text', 'There is a problem'); - }, - ); - }); - }); -}); diff --git a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js index b4bfdfbf3..11e441100 100644 --- a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js @@ -128,7 +128,7 @@ describe('GP Workflow: View Lloyd George record', () => { cy.get('#verify-submit').click(); cy.wait('@stitchJobCompleted', { timeout: 20000 }); - cy.getByTestId('download-all-files-link').click(); + cy.getByTestId('download-files-link').click(); }; context('Download Lloyd George document', () => { @@ -153,8 +153,8 @@ describe('GP Workflow: View Lloyd George record', () => { cy.wait('@stitchJobCompleted', { timeout: 20000 }); cy.title().should('eq', lloydGeorgeRecordPageTitle); - cy.getByTestId('download-all-files-link', { timeout: 20000 }).should('exist'); - cy.getByTestId('download-all-files-link').click(); + cy.getByTestId('download-files-link', { timeout: 20000 }).should('exist'); + cy.getByTestId('download-files-link').click(); // Select documents page cy.title().should('eq', downloadPageTitle); @@ -270,8 +270,8 @@ describe('GP Workflow: View Lloyd George record', () => { cy.wait('@stitchJobCompleted', { timeout: 20000 }); cy.title().should('eq', lloydGeorgeRecordPageTitle); - cy.getByTestId('download-all-files-link').should('exist'); - cy.getByTestId('download-all-files-link').click(); + cy.getByTestId('download-files-link').should('exist'); + cy.getByTestId('download-files-link').click(); // Select documents page cy.title().should('eq', downloadPageTitle); @@ -384,7 +384,7 @@ describe('GP Workflow: View Lloyd George record', () => { cy.get('#verify-submit').click(); cy.wait('@stitchJobPostEmpty', { timeout: 20000 }); - cy.getByTestId('download-all-files-link').should('not.exist'); + cy.getByTestId('download-files-link').should('not.exist'); }, ); @@ -399,7 +399,7 @@ describe('GP Workflow: View Lloyd George record', () => { cy.get('#verify-submit').click(); cy.wait('@stitchJobCompleted', { timeout: 20000 }); - cy.getByTestId('download-all-files-link').should('not.exist'); + cy.getByTestId('download-files-link').should('not.exist'); }, ); @@ -418,8 +418,8 @@ describe('GP Workflow: View Lloyd George record', () => { cy.get('#verify-submit').click(); cy.wait('@stitchJobCompleted', { timeout: 20000 }); - cy.getByTestId('download-all-files-link').should('exist'); - cy.getByTestId('download-all-files-link').click(); + cy.getByTestId('download-files-link').should('exist'); + cy.getByTestId('download-files-link').click(); cy.wait('@documentManifest'); diff --git a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/patient_search_and_verify_workflow.cy.js b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/patient_search_and_verify_workflow.cy.js index 9825b50f8..853bc0660 100644 --- a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/patient_search_and_verify_workflow.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/patient_search_and_verify_workflow.cy.js @@ -148,43 +148,6 @@ describe('GP Workflow: Patient search and verify', () => { ); }); - // skipped as arf journey is not viable at this point in time - it.skip( - `Shows patient upload screen when patient search is used as a - GP_ADMIN and patient response is inactive`, - { tags: 'regression' }, - () => { - setup(Roles.GP_ADMIN); - cy.intercept('GET', '/SearchPatient*', { - statusCode: 200, - body: patient, - }).as('search'); - - cy.get('#nhs-number-input').click(); - cy.get('#nhs-number-input').type(testPatient); - cy.title().should( - 'eq', - 'Search for a patient - Access and store digital patient documents', - ); - - cy.get('#search-submit').click(); - cy.wait('@search'); - cy.title().should('eq', 'Patient details - Access and store digital patient documents'); - - cy.url().should('include', 'verify'); - cy.url().should('eq', baseUrl + routes.patientVerify); - cy.get('#gp-message').should('be.visible'); - cy.get('#gp-message').should( - 'have.text', - 'This page displays the current data recorded in the Personal Demographics Service for this patient.', - ); - cy.get('#verify-submit').click(); - - cy.url().should('include', 'upload'); - cy.url().should('eq', baseUrl + routes.arfUpload); - }, - ); - it( 'Does not show the upload documents page when upload patient is verified and inactive as a GP_Clinical', { tags: 'regression' }, diff --git a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/view_lloyd_george_workflow.cy.js b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/view_lloyd_george_workflow.cy.js index ab872381e..ec8211d3c 100644 --- a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/view_lloyd_george_workflow.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/view_lloyd_george_workflow.cy.js @@ -2,6 +2,7 @@ import viewLloydGeorgePayload from '../../../fixtures/requests/GET_LloydGeorgeSt import searchPatientPayload from '../../../fixtures/requests/GET_SearchPatient.json'; import { Roles, roleName } from '../../../support/roles'; import { formatNhsNumber } from '../../../../src/helpers/utils/formatNhsNumber'; +import { DOCUMENT_TYPE } from '../../../../src/helpers/utils/documentType'; const baseUrl = Cypress.config('baseUrl'); const gpRoles = [Roles.GP_ADMIN, Roles.GP_CLINICAL]; @@ -279,8 +280,8 @@ describe('GP Workflow: View Lloyd George record', () => { cy.get('#verify-submit').click(); cy.wait('@stitchJobCompleted', { timeout: 20000 }); - cy.getByTestId('delete-all-files-link').should('exist'); - cy.getByTestId('delete-all-files-link').click(); + cy.getByTestId('delete-files-link').should('exist'); + cy.getByTestId('delete-files-link').click(); cy.wait('@searchDocs'); // assert delete confirmation page is as expected @@ -294,7 +295,7 @@ describe('GP Workflow: View Lloyd George record', () => { cy.intercept( 'DELETE', - `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=LG`, + `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=${DOCUMENT_TYPE.LLOYD_GEORGE}`, { statusCode: 200, body: 'Success', @@ -349,8 +350,8 @@ describe('GP Workflow: View Lloyd George record', () => { cy.get('#verify-submit').click(); cy.wait('@stitchJobCompleted', { timeout: 20000 }); - cy.getByTestId('delete-all-files-link').should('exist'); - cy.getByTestId('delete-all-files-link').click(); + cy.getByTestId('delete-files-link').should('exist'); + cy.getByTestId('delete-files-link').click(); // cancel delete cy.wait('@searchDocs'); @@ -384,7 +385,7 @@ describe('GP Workflow: View Lloyd George record', () => { cy.intercept( 'DELETE', - `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=LG`, + `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=${DOCUMENT_TYPE.LLOYD_GEORGE}`, { statusCode: 500, body: 'Failed to delete documents', @@ -394,8 +395,8 @@ describe('GP Workflow: View Lloyd George record', () => { cy.get('#verify-submit').click(); cy.wait('@stitchJobCompleted', { timeout: 20000 }); - cy.getByTestId('delete-all-files-link').should('exist'); - cy.getByTestId('delete-all-files-link').click(); + cy.getByTestId('delete-files-link').should('exist'); + cy.getByTestId('delete-files-link').click(); cy.wait('@searchDocs'); @@ -423,7 +424,7 @@ describe('GP Workflow: View Lloyd George record', () => { cy.get('#verify-submit').click(); cy.wait('@lloydGeorgeStitch', { timeout: 20000 }); - cy.getByTestId('download-all-files-link').should('not.exist'); + cy.getByTestId('download-files-link').should('not.exist'); }, ); @@ -437,7 +438,7 @@ describe('GP Workflow: View Lloyd George record', () => { cy.get('#verify-submit').click(); cy.wait('@stitchJobCompleted', { timeout: 20000 }); - cy.getByTestId('download-all-files-link').should('not.exist'); + cy.getByTestId('download-files-link').should('not.exist'); }, ); }); diff --git a/app/cypress/e2e/0-ndr-core-tests/pcse_user_workflows/download_patient_files_workflow.cy.js b/app/cypress/e2e/0-ndr-core-tests/pcse_user_workflows/download_patient_files_workflow.cy.js index 7bc2424e1..bc7337aa0 100644 --- a/app/cypress/e2e/0-ndr-core-tests/pcse_user_workflows/download_patient_files_workflow.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/pcse_user_workflows/download_patient_files_workflow.cy.js @@ -1,27 +1,10 @@ import searchPatientPayload from '../../../fixtures/requests/GET_SearchPatient.json'; import { Roles } from '../../../support/roles'; import { formatNhsNumber } from '../../../../src/helpers/utils/formatNhsNumber'; +import { DOCUMENT_TYPE } from '../../../../src/helpers/utils/documentType'; describe('PCSE Workflow: Access and download found files', () => { - // env vars - const baseUrl = Cypress.config('baseUrl'); - - const roles = Object.freeze({ - GP: 'GP_ADMIN', - PCSE: 'PCSE', - }); - const testPatient = '9000000009'; - const patient = { - birthDate: new Date('1970-01-01'), - familyName: 'Default Surname', - givenName: ['Default Given Name'], - nhsNumber: testPatient, - postalCode: 'AA1 1AA', - superseded: false, - restricted: false, - deceased: false, - }; const searchDocumentReferencesResponse = [ { @@ -43,185 +26,6 @@ describe('PCSE Workflow: Access and download found files', () => { cy.navigateToPatientSearchPage(); }); - const navigateToVerify = () => { - cy.intercept('GET', '/SearchPatient*', { - statusCode: 200, - body: patient, - }).as('search'); - - cy.getByTestId('nhs-number-input').click(); - cy.getByTestId('nhs-number-input').type(testPatient); - cy.getByTestId('search-submit-btn').click(); - cy.wait('@search'); - }; - - const navigateToDownload = () => { - navigateToVerify(); - cy.get('#verify-submit').click(); - }; - - // it('shows patient details on download page', { tags: 'regression' }, () => { - // navigateToDownload(); - - // cy.get('#download-page-title').should('have.length', 1); - // cy.get('#patient-details-nhs-number').should('have.text', patient.nhsNumber); - // cy.get('#patient-details-family-name').should('have.text', patient.familyName); - - // const givenName = patient.givenName[0]; - // cy.get('#patient-details-given-name').should('have.text', givenName + ' '); - // cy.get('#patient-details-date-of-birth').should( - // 'have.text', - // patient.birthDate.toLocaleDateString('en-GB', { - // day: '2-digit', - // month: 'long', - // year: 'numeric', - // }), - // ); - // cy.get('#patient-details-postcode').should('have.text', patient.postalCode); - // }); - - // it('shows no files avaliable on 204 success', { tags: 'regression' }, () => { - // const searchDocumentReferencesResponse = []; - - // cy.intercept('GET', '/SearchDocumentReferences*', { - // statusCode: 204, - // body: searchDocumentReferencesResponse, - // }).as('search'); - - // navigateToDownload(roles.PCSE); - - // cy.get('#no-files-message').should('have.length', 1); - // cy.get('#no-files-message').should( - // 'have.text', - // 'There are no documents available for this patient.', - // ); - // }); - - // it('shows avaliable files to download on 200 success', { tags: 'regression' }, () => { - // cy.intercept('GET', '/SearchDocumentReferences*', { - // statusCode: 200, - // body: searchDocumentReferencesResponse, - // }).as('search'); - - // navigateToDownload(roles.PCSE); - - // cy.get('#available-files-table-title').should('have.length', 1); - - // cy.get('.available-files-row').should('have.length', 2); - // cy.get('#available-files-row-0-filename').should( - // 'have.text', - // searchDocumentReferencesResponse[1].fileName, - // ); - // cy.get('#available-files-row-1-filename').should( - // 'have.text', - // searchDocumentReferencesResponse[0].fileName, - // ); - - // cy.get('#available-files-row-0-created-date').should('exist'); - // cy.get('#available-files-row-1-created-date').should('exist'); - - // // We cannot test datetimes of a created s3 bucket object easily on a live instance, therefore - - // cy.get('#available-files-row-0-created-date').should( - // 'have.text', - // searchDocumentReferencesResponse[1].created.toLocaleDateString('en-GB', { - // day: '2-digit', - // month: 'long', - // year: 'numeric', - // hour: 'numeric', - // minute: 'numeric', - // second: 'numeric', - // }), - // ); - // cy.get('#available-files-row-1-created-date').should( - // 'have.text', - // searchDocumentReferencesResponse[0].created.toLocaleDateString('en-GB', { - // day: '2-digit', - // month: 'long', - // year: 'numeric', - // hour: 'numeric', - // minute: 'numeric', - // second: 'numeric', - // }), - // ); - // }); - - // it( - // 'Shows spinner button while waiting for Download Document Manifest response', - // { tags: 'regression' }, - // () => { - // cy.intercept('GET', '/SearchDocumentReferences*', { - // statusCode: 200, - // body: searchDocumentReferencesResponse, - // }).as('search'); - - // navigateToDownload(roles.PCSE); - - // const documentManifestResponse = 'test-s3-url'; - // cy.intercept({ url: '/DocumentManifest*', middleware: true }, (req) => { - // req.reply({ - // statusCode: 200, - // body: documentManifestResponse, - // delay: 1500, - // }); - // }).as('search'); - - // cy.get('#download-documents').click(); - // cy.get('#download-spinner').should('exist'); - // }, - // ); - - // it( - // 'Shows service error box on Search Document Reference 500 response', - // { tags: 'regression' }, - // () => { - // cy.intercept('GET', '/SearchDocumentReferences*', { - // statusCode: 500, - // }).as('search'); - - // navigateToDownload(roles.PCSE); - - // cy.contains('Sorry, there is a problem with the service').should('be.visible'); - // }, - // ); - - // it( - // 'Shows progress bar while waiting for Search Document Reference response', - // { tags: 'regression' }, - // () => { - // const searchDocumentReferencesResponse = []; - - // cy.intercept({ url: '/SearchDocumentReferences*', middleware: true }, (req) => { - // req.reply({ - // statusCode: 204, - // body: searchDocumentReferencesResponse, - // delay: 1500, - // }); - // }).as('search'); - - // navigateToDownload(roles.PCSE); - - // cy.get('.progress-bar').should('exist'); - // }, - // ); - - // it('Start again button takes us to the home page', { tags: 'regression' }, () => { - // const searchDocumentReferencesResponse = []; - - // cy.intercept({ url: '/SearchDocumentReferences*', middleware: true }, (req) => { - // req.reply({ - // statusCode: 204, - // body: searchDocumentReferencesResponse, - // }); - // }).as('search'); - - // navigateToDownload(roles.PCSE); - - // cy.get('#start-again-link').should('exist'); - // cy.get('#start-again-link').click(); - // cy.url().should('eq', baseUrl + homeUrl); - // }); - context('Delete all documents relating to a patient', () => { beforeEach(() => { cy.intercept('GET', '/SearchPatient*', { @@ -250,7 +54,7 @@ describe('PCSE Workflow: Access and download found files', () => { () => { cy.intercept( 'DELETE', - `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=LG,ARF`, + `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=${DOCUMENT_TYPE.ALL}`, { statusCode: 200, body: 'Success', @@ -285,7 +89,7 @@ describe('PCSE Workflow: Access and download found files', () => { cy.getByTestId('delete-submit-btn').click(); // assert user is returned to download documents page - cy.contains('Manage this Lloyd George record').should('be.visible'); + cy.contains('Manage Lloyd George records').should('be.visible'); }, ); @@ -295,7 +99,7 @@ describe('PCSE Workflow: Access and download found files', () => { () => { cy.intercept( 'DELETE', - `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=LG,ARF`, + `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=${DOCUMENT_TYPE.ALL}`, { statusCode: 500, body: 'Failed to delete documents', @@ -319,7 +123,7 @@ describe('PCSE Workflow: Access and download found files', () => { () => { cy.intercept( 'DELETE', - `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=LG,ARF`, + `/DocumentDelete?patientId=${searchPatientPayload.nhsNumber}&docType=${DOCUMENT_TYPE.ALL}`, { statusCode: 404, body: 'No documents available', diff --git a/app/cypress/e2e/0-ndr-core-tests/pcse_user_workflows/patient_search_and_verify_workflow.cy.js b/app/cypress/e2e/0-ndr-core-tests/pcse_user_workflows/patient_search_and_verify_workflow.cy.js index b91042b13..678673b80 100644 --- a/app/cypress/e2e/0-ndr-core-tests/pcse_user_workflows/patient_search_and_verify_workflow.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/pcse_user_workflows/patient_search_and_verify_workflow.cy.js @@ -5,7 +5,7 @@ describe('PCSE Workflow: patient search and verify', () => { const baseUrl = Cypress.config('baseUrl'); const patientVerifyUrl = '/patient/verify'; - const arfDownloadUrl = '/patient/arf'; + const documentsUrl = '/patient/documents'; const homeUrl = '/'; const patient = { @@ -35,13 +35,20 @@ describe('PCSE Workflow: patient search and verify', () => { body: patient, }).as('search'); + cy.intercept('GET', '/SearchDocumentReferences*', { + statusCode: 200, + body: [], + }).as('documentSearch'); + cy.get('#nhs-number-input').click(); cy.get('#nhs-number-input').type(testPatient); cy.get('#search-submit').click(); cy.wait('@search'); cy.get('#verify-submit').click(); - cy.url().should('eq', baseUrl + arfDownloadUrl); + cy.wait('@documentSearch'); + + cy.url().should('eq', baseUrl + documentsUrl); }, ); diff --git a/app/cypress/e2e/1-ndr-smoke-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js b/app/cypress/e2e/1-ndr-smoke-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js index cdf64f845..c7759b3b6 100644 --- a/app/cypress/e2e/1-ndr-smoke-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js +++ b/app/cypress/e2e/1-ndr-smoke-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js @@ -47,7 +47,7 @@ describe('GP Workflow: View Lloyd George record', () => { cy.getByTestId('pdf-viewer', { timeout: 30000 }).should('be.visible'); - cy.getByTestId('download-all-files-link').click(); + cy.getByTestId('download-files-link').click(); cy.getByTestId('available-files-table-title').should('exist'); cy.getByTestId('download-file-btn').click(); diff --git a/app/cypress/support/e2e.ts b/app/cypress/support/e2e.ts index 9487aabec..eed08911d 100644 --- a/app/cypress/support/e2e.ts +++ b/app/cypress/support/e2e.ts @@ -13,7 +13,7 @@ import { defaultFeatureFlags } from './feature_flags'; import './aws.commands'; import 'cypress-real-events'; -const { register: registerCypressGrep } = require('@cypress/grep') +const { register: registerCypressGrep } = require('@cypress/grep'); registerCypressGrep(); const roleEntries = Object.entries(Roles) as [RoleKey, RoleId][]; diff --git a/app/cypress/support/feature_flags.ts b/app/cypress/support/feature_flags.ts index 74dd5f2fe..9db9afeb9 100644 --- a/app/cypress/support/feature_flags.ts +++ b/app/cypress/support/feature_flags.ts @@ -7,4 +7,5 @@ export const defaultFeatureFlags: FeatureFlags = { uploadArfWorkflowEnabled: true, uploadLambdaEnabled: true, uploadDocumentIteration2Enabled: true, + uploadDocumentIteration3Enabled: false, }; diff --git a/app/cypress/support/routes.ts b/app/cypress/support/routes.ts index b7a24068d..14657db77 100644 --- a/app/cypress/support/routes.ts +++ b/app/cypress/support/routes.ts @@ -5,6 +5,5 @@ export enum routes { createReport = '/create-report', createReportComplete = '/create-report/complete', lloydGeorgeView = '/patient/lloyd-george-record', - arfUpload = '/patient/arf/upload', sessionExpired = '/session-expired', } diff --git a/app/package.json b/app/package.json index 8280c07b4..9f8792e06 100644 --- a/app/package.json +++ b/app/package.json @@ -7,6 +7,7 @@ "build": "tsc --noEmit && vite build", "serve": "vite preview", "build-env-check": "node ./react-build-env-checker.js && vite build", + "lint": "eslint \"src/**/*.+(ts|tsx|js)\"", "test": "vitest", "test-all": "vitest --run", "test-all:coverage": "vitest --run --coverage src", diff --git a/app/src/components/blocks/_arf/completeStage/CompleteStage.test.tsx b/app/src/components/blocks/_arf/completeStage/CompleteStage.test.tsx deleted file mode 100644 index 1c344842e..000000000 --- a/app/src/components/blocks/_arf/completeStage/CompleteStage.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { - DOCUMENT_TYPE, - DOCUMENT_UPLOAD_STATE as documentUploadStates, - UploadDocument, -} from '../../../../types/pages/UploadDocumentsPage/types'; -import { buildPatientDetails, buildTextFile } from '../../../../helpers/test/testBuilders'; -import CompleteStage from './CompleteStage'; -import { useNavigate } from 'react-router-dom'; -import usePatient from '../../../../helpers/hooks/usePatient'; -import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; -import { afterEach, beforeEach, describe, expect, it, vi, Mock } from 'vitest'; - -vi.mock('react-router-dom'); -vi.mock('../../../../helpers/hooks/usePatient'); - -const mockedUsePatient = usePatient as Mock; -const mockPatientDetails = buildPatientDetails(); - -describe('', () => { - beforeEach(() => { - import.meta.env.VITE_ENVIRONMENT = 'vitest'; - mockedUsePatient.mockReturnValue(mockPatientDetails); - }); - afterEach(() => { - vi.clearAllMocks(); - }); - - describe('Show complete stage', () => { - it('with successfully uploaded docs', async () => { - const navigateMock = vi.fn(); - const documentOne: UploadDocument = { - file: buildTextFile('one', 100), - progress: 0, - state: documentUploadStates.FAILED, - id: '1', - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - const documentTwo: UploadDocument = { - file: buildTextFile('two', 200), - progress: 0, - state: documentUploadStates.SUCCEEDED, - id: '2', - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - const documentThree: UploadDocument = { - file: buildTextFile('three', 100), - progress: 0, - state: documentUploadStates.SUCCEEDED, - id: '3', - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - - // @ts-ignore - useNavigate.mockImplementation(() => navigateMock); - const documents: Array = [documentOne, documentTwo, documentThree]; - render(); - expect( - await screen.findByRole('heading', { name: 'Upload Summary' }), - ).toBeInTheDocument(); - - await userEvent.click(screen.getByLabelText('View successfully uploaded documents')); - - expect(screen.getByText(documentTwo.file.name)).toBeInTheDocument(); - expect(screen.getByText(documentThree.file.name)).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'There is a problem' })).toBeInTheDocument(); - expect(screen.getByText(`1 of 3 files failed to upload`)).toBeInTheDocument(); - expect( - screen.getByText("If you want to upload another patient's health record"), - ).toBeInTheDocument(); - - await userEvent.click(screen.getByRole('button', { name: 'Start Again' })); - - expect(navigateMock).toHaveBeenCalledWith('/'); - }); - }); - - it('pass accessibility checks', async () => { - render(); - expect(await screen.findByRole('heading', { name: 'Upload Summary' })).toBeInTheDocument(); - - const results = await runAxeTest(document.body); - expect(results).toHaveNoViolations(); - }); -}); diff --git a/app/src/components/blocks/_arf/completeStage/CompleteStage.tsx b/app/src/components/blocks/_arf/completeStage/CompleteStage.tsx deleted file mode 100644 index fd98cbb2c..000000000 --- a/app/src/components/blocks/_arf/completeStage/CompleteStage.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { Button } from 'nhsuk-react-components'; -import { useNavigate } from 'react-router-dom'; -import { UploadDocument } from '../../../../types/pages/UploadDocumentsPage/types'; -import UploadSummary from '../uploadSummary/UploadSummary'; -interface Props { - documents: Array; -} - -const CompleteStage = ({ documents }: Props): React.JSX.Element => { - const navigate = useNavigate(); - - return ( - <> - -

- If you want to upload another patient's health record -

- - - ); -}; - -export default CompleteStage; diff --git a/app/src/components/blocks/_arf/documentInputForm/DocumentInputForm.tsx b/app/src/components/blocks/_arf/documentInputForm/DocumentInputForm.tsx deleted file mode 100644 index aad788d63..000000000 --- a/app/src/components/blocks/_arf/documentInputForm/DocumentInputForm.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { TextInput, Table, WarningCallout } from 'nhsuk-react-components'; -import React from 'react'; -import { - DOCUMENT_TYPE, - FileInputEvent, - UploadDocument, -} from '../../../../types/pages/UploadDocumentsPage/types'; -import formatFileSize from '../../../../helpers/utils/formatFileSize'; -import { FieldValues, UseControllerReturn } from 'react-hook-form'; - -type Props = { - documents: Array; - formController: UseControllerReturn; - inputRef: React.RefObject; - onDocumentRemove: (index: number, docType: DOCUMENT_TYPE) => void; - onDocumentInput: (e: FileInputEvent, docType: DOCUMENT_TYPE) => void; - formType: DOCUMENT_TYPE; - showHelp?: boolean; -}; - -const DocumentInputForm = ({ - documents, - onDocumentRemove, - onDocumentInput, - formController, - inputRef, - formType, - showHelp = false, -}: Props): React.JSX.Element => { - const hasDuplicateFiles = documents.some((doc: UploadDocument) => { - return documents.some( - (compare: UploadDocument) => - doc.file.name === compare.file.name && - doc.file.size === compare.file.size && - doc.id !== compare.id, - ); - }); - return ( - <> - onDocumentInput(e, formType)} - onBlur={formController.field.onBlur} - // @ts-ignore The NHS Component library is outdated and does not allow for any reference other than a blank MutableRefObject - inputRef={(e: HTMLInputElement): void => { - formController.field.ref(e); - inputRef.current = e; - }} - //@ts-ignore - hint={ -
    -
  • - {formType === DOCUMENT_TYPE.LLOYD_GEORGE - ? 'If you are uploading a patient’s Lloyd George envelope you must upload the full Lloyd George envelope including the front and back of the envelope.' - : "A patient's full electronic health record including attachments must be uploaded."} -
  • -
  • You can select multiple files to upload at once.
  • - {showHelp && ( -
  • - In the event documents cannot be uploaded, they must be printed and - sent via{' '} - - Primary Care Support England - - {'.'} -
  • - )} - {formType === DOCUMENT_TYPE.LLOYD_GEORGE && ( - <> -
  • - Each Lloyd George file uploaded must match the following format: - [PDFnumber]_Lloyd_George_Record_[Patient Name]_[NHS - Number]_[D.O.B]. For example: 1of2_Lloyd_George_Record_[Joe - Bloggs]_[123456789]_[25-12-2019] -
  • -
  • You can only upload PDF files
  • - - )} -
- } - /> -
- {documents && documents.length > 0 && ( - - - - Filename - Size - Remove - - - - - {documents.map((document: UploadDocument, index: number) => ( - - {document.file.name} - {formatFileSize(document.file.size)} - - - - - ))} - -
- )} - {hasDuplicateFiles && formType === DOCUMENT_TYPE.ARF && ( - - Possible duplicate file -

There are two or more documents with the same name.

-

Are you sure you want to proceed?

-
- )} -
- - ); -}; - -export default DocumentInputForm; diff --git a/app/src/components/blocks/_arf/documentSearchResults/DocumentSearchResults.test.tsx b/app/src/components/blocks/_arf/documentSearchResults/DocumentSearchResults.test.tsx deleted file mode 100644 index 2e7d80f75..000000000 --- a/app/src/components/blocks/_arf/documentSearchResults/DocumentSearchResults.test.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { buildSearchResult } from '../../../../helpers/test/testBuilders'; -import { getFormattedDatetime } from '../../../../helpers/utils/formatDatetime'; -import { SearchResult } from '../../../../types/generic/searchResult'; -import DocumentSearchResults from './DocumentSearchResults'; -import { render, screen, within } from '@testing-library/react'; -import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; -import { describe, expect, it } from 'vitest'; - -describe('DocumentSearchResults', () => { - const mockDetails = buildSearchResult(); - - const mockSearchResults: Array = [mockDetails]; - - it('renders provided search results information', () => { - render(); - - expect(screen.getByText('List of documents available')).toBeInTheDocument(); - const searchResults = screen.getAllByTestId('search-result'); - - const mappedResults = searchResults.map((result) => ({ - filename: within(result).getByTestId('filename').textContent, - created: within(result).getByTestId('created').textContent, - })); - - expect(mappedResults).toEqual([ - { - filename: mockDetails.fileName, - created: getFormattedDatetime(new Date(mockDetails.created)), - }, - ]); - }); - - it('renders provided search results in order of date', () => { - const oldestDate = new Date(Date.UTC(2023, 7, 9, 10)).toISOString(); - const secondOldestDate = new Date(Date.UTC(2023, 7, 10, 10)).toISOString(); - const newestDate = new Date(Date.UTC(2023, 7, 11, 10)).toISOString(); - - const mockSearchResults = [ - buildSearchResult({ created: oldestDate }), - buildSearchResult({ created: newestDate }), - buildSearchResult({ created: secondOldestDate }), - ]; - - render(); - - expect(screen.getByText('List of documents available')).toBeInTheDocument(); - - const searchResults = screen.getAllByTestId('search-result'); - - const mappedResults = searchResults.map((result) => ({ - created: within(result).getByTestId('created').textContent, - })); - - expect(mappedResults).toEqual([ - { created: getFormattedDatetime(new Date(newestDate)) }, - { created: getFormattedDatetime(new Date(secondOldestDate)) }, - { created: getFormattedDatetime(new Date(oldestDate)) }, - ]); - }); - - it('pass accessibility checks', async () => { - render(); - await screen.findByText(mockDetails.fileName); - - const results = await runAxeTest(document.body); - expect(results).toHaveNoViolations(); - }); -}); diff --git a/app/src/components/blocks/_arf/documentSearchResults/DocumentSearchResults.tsx b/app/src/components/blocks/_arf/documentSearchResults/DocumentSearchResults.tsx deleted file mode 100644 index 736fdc535..000000000 --- a/app/src/components/blocks/_arf/documentSearchResults/DocumentSearchResults.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Table } from 'nhsuk-react-components'; -import { SearchResult } from '../../../../types/generic/searchResult'; -import { getFormattedDatetime } from '../../../../helpers/utils/formatDatetime'; - -type Props = { - searchResults: Array; -}; - -const DocumentSearchResults = (props: Props): React.JSX.Element => { - const sortMethod = (a: SearchResult, b: SearchResult): number => - new Date(a.created) < new Date(b.created) ? 1 : -1; - - const orderedResults = [...props.searchResults].sort(sortMethod); - const tableCaption = ( -

List of documents available

- ); - - return ( - - - - Filename - Uploaded At - - - - {orderedResults.map((result, index) => ( - - - {result.fileName} - - - {getFormattedDatetime(new Date(result.created))} - - - ))} - -
- ); -}; - -export default DocumentSearchResults; diff --git a/app/src/components/blocks/_arf/selectStage/SelectStage.test.tsx b/app/src/components/blocks/_arf/selectStage/SelectStage.test.tsx deleted file mode 100644 index 4e6627426..000000000 --- a/app/src/components/blocks/_arf/selectStage/SelectStage.test.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import SelectStage from './SelectStage'; -import { buildPatientDetails, buildTextFile } from '../../../../helpers/test/testBuilders'; -import userEvent from '@testing-library/user-event'; -import { - DOCUMENT_UPLOAD_STATE as documentUploadStates, - UploadDocument, -} from '../../../../types/pages/UploadDocumentsPage/types'; -import { PatientDetails } from '../../../../types/generic/patientDetails'; -import usePatient from '../../../../helpers/hooks/usePatient'; -import { formatNhsNumber } from '../../../../helpers/utils/formatNhsNumber'; -import { useState } from 'react'; -import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; -import { afterEach, beforeEach, describe, expect, it, vi, Mock } from 'vitest'; - -const mockedUseNavigate = vi.fn(); - -vi.mock('../../../../helpers/requests/uploadDocuments'); -vi.mock('../../../../helpers/hooks/useBaseAPIHeaders'); -vi.mock('../../../../helpers/hooks/useBaseAPIUrl'); -vi.mock('../../../../helpers/utils/toFileList'); -vi.mock('../../../../helpers/hooks/usePatient'); -vi.mock('react-router-dom', () => ({ - useNavigate: () => mockedUseNavigate, - useLocation: () => vi.fn(), -})); -const mockedUsePatient = usePatient as Mock; -const mockPatient = buildPatientDetails(); -const documentOne = buildTextFile('one', 100); -const documentTwo = buildTextFile('two', 200); -const documentThree = buildTextFile('three', 100); -const arfDocuments = [documentOne, documentTwo, documentThree]; - -const setDocumentMock = vi.fn(); -setDocumentMock.mockImplementation((document) => { - document.state = documentUploadStates.SELECTED; - document.id = '1'; -}); - -const mockStartUpload = vi.fn(); - -const mockPatientDetails: PatientDetails = buildPatientDetails(); -describe('', () => { - beforeEach(() => { - import.meta.env.VITE_ENVIRONMENT = 'vitest'; - mockedUsePatient.mockReturnValue(mockPatient); - }); - afterEach(() => { - vi.clearAllMocks(); - }); - - describe('Rendering', () => { - it('renders the page', async () => { - renderApp(); - - expect(screen.getByRole('heading', { name: 'Upload documents' })).toBeInTheDocument(); - const expectedNhsNumber = formatNhsNumber(mockPatientDetails.nhsNumber); - expect(screen.getByText(expectedNhsNumber)).toBeInTheDocument(); - expect(screen.getByText('Select file(s)')).toBeInTheDocument(); - - expect(screen.getByRole('button', { name: 'Upload' })).toBeInTheDocument(); - }); - - it.skip('does upload and then remove a file', async () => { - renderApp(); - await userEvent.upload(screen.getByTestId('ARF-input'), [ - documentOne, - documentTwo, - documentThree, - ]); - - expect(screen.getByText(documentOne.name)).toBeInTheDocument(); - - const removeFile = await screen.findByRole('button', { - name: `Remove ${documentOne.name} from selection`, - }); - - await userEvent.click(removeFile); - - expect(screen.queryByText(documentOne.name)).not.toBeInTheDocument(); - expect(screen.getByText(documentTwo.name)).toBeInTheDocument(); - expect(screen.getByText(documentThree.name)).toBeInTheDocument(); - }); - - it('does not upload either forms if selected file is more than 5GB', async () => { - renderApp(); - const documentBig = buildTextFile('four', 6 * Math.pow(1024, 3)); - const documents = [...arfDocuments, documentBig]; - - await userEvent.upload(screen.getByTestId('ARF-input'), documents); - - expect(screen.getByText(documentBig.name)).toBeInTheDocument(); - - await userEvent.click(screen.getByText('Upload')); - - expect( - await screen.findByText('Please ensure that all files are less than 5GB in size'), - ).toBeInTheDocument(); - }); - - it.skip('shows a duplicate file warning if two or more files match name/size for ARF input only', async () => { - const duplicateFileWarning = 'There are two or more documents with the same name.'; - renderApp(); - - await userEvent.upload(screen.getByTestId('ARF-input'), [documentOne, documentOne]); - - await screen.findByText(duplicateFileWarning); - - const removeButtons = await screen.findAllByRole('button', { - name: `Remove ${documentOne.name} from selection`, - }); - - userEvent.click(removeButtons[1]); - - await waitFor(() => { - expect(screen.queryByText(duplicateFileWarning)).not.toBeInTheDocument(); - }); - }); - - it.skip("does allow the user to add the same file again if they remove for '%s' input", async () => { - renderApp(); - const selectFilesLabel = screen.getByTestId('ARF-input'); - - await userEvent.upload(selectFilesLabel, documentOne); - - const removeFile = await screen.findByRole('button', { - name: `Remove ${documentOne.name} from selection`, - }); - - await userEvent.click(removeFile); - await userEvent.upload(selectFilesLabel, documentOne); - - expect(await screen.findByText(documentOne.name)).toBeInTheDocument(); - }); - - it('show an alert message when user try to upload with no files selected', async () => { - renderApp(); - await userEvent.click(screen.getByRole('button', { name: 'Upload' })); - expect(await screen.findByText('Select a file to upload')).toBeInTheDocument(); - }); - - it('renders link to PCSE that opens in a new tab', () => { - renderApp(); - const pcseLink = screen.getByRole('link', { - name: 'Primary Care Support England - this link will open in a new tab', - }); - expect(pcseLink).toHaveAttribute('href', 'https://secure.pcse.england.nhs.uk/'); - expect(pcseLink).toHaveAttribute('target', '_blank'); - }); - }); - - describe('Accessibility', () => { - it('pass accessibility check when some files are selected', async () => { - renderApp(); - - const selectFilesLabel = screen.getByTestId(`ARF-input`); - await userEvent.upload(selectFilesLabel, documentOne); - - const results = await runAxeTest(document.body); - expect(results).toHaveNoViolations(); - }); - }); - - describe('Navigation', () => { - it('calls startUpload if user selected some files and clicked upload button', async () => { - renderApp(); - userEvent.upload(screen.getByTestId('ARF-input'), [ - documentOne, - documentTwo, - documentThree, - ]); - await userEvent.click(screen.getByRole('button', { name: 'Upload' })); - - await waitFor(() => { - expect(mockStartUpload).toHaveBeenCalledTimes(1); - }); - }); - }); - - const renderApp = () => { - render(); - }; - - const TestApp = () => { - const [documents, setDocuments] = useState>([]); - return ( - - ); - }; -}); diff --git a/app/src/components/blocks/_arf/selectStage/SelectStage.tsx b/app/src/components/blocks/_arf/selectStage/SelectStage.tsx deleted file mode 100644 index 7de0c878e..000000000 --- a/app/src/components/blocks/_arf/selectStage/SelectStage.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { useRef } from 'react'; -import { - DOCUMENT_TYPE, - DOCUMENT_UPLOAD_STATE, - FileInputEvent, - SetUploadDocuments, - UploadDocument, -} from '../../../../types/pages/UploadDocumentsPage/types'; -import { Button, Fieldset } from 'nhsuk-react-components'; -import { useController, useForm } from 'react-hook-form'; -import toFileList from '../../../../helpers/utils/toFileList'; -import DocumentInputForm from '../documentInputForm/DocumentInputForm'; -import { ARFFormConfig } from '../../../../helpers/utils/formConfig'; -import { v4 as uuidv4 } from 'uuid'; -import BackButton from '../../../generic/backButton/BackButton'; -import PatientSummary from '../../../generic/patientSummary/PatientSummary'; - -interface Props { - setDocuments: SetUploadDocuments; - documents: Array; - startUpload: () => Promise; -} - -const SelectStage = ({ - setDocuments, - documents, - startUpload, -}: Readonly): React.JSX.Element => { - const arfInputRef = useRef(null); - - const hasFileInput = documents.length > 0; - - const { handleSubmit, control, formState, setError } = useForm(); - const arfController = useController(ARFFormConfig(control)); - - const submitDocuments = async (): Promise => { - if (!hasFileInput) { - setError('arf-documents', { type: 'custom', message: 'Select a file to upload' }); - return; - } - - await startUpload(); - }; - - const onInput = (e: FileInputEvent, docType: DOCUMENT_TYPE): void => { - const fileArray = Array.from(e.target.files ?? new FileList()); - const newlyAddedDocuments: Array = fileArray.map((file) => ({ - id: uuidv4(), - file, - state: DOCUMENT_UPLOAD_STATE.SELECTED, - progress: 0, - docType: docType, - attempts: 0, - })); - const updatedDocList = [...newlyAddedDocuments, ...documents]; - arfController.field.onChange(updatedDocList); - setDocuments(updatedDocList); - }; - - const onRemove = (index: number, _docType: DOCUMENT_TYPE): void => { - const updatedDocList: UploadDocument[] = [ - ...documents.slice(0, index), - ...documents.slice(index + 1), - ]; - - if (arfInputRef.current) { - arfInputRef.current.files = toFileList(updatedDocList); - arfController.field.onChange(updatedDocList); - } - - setDocuments(updatedDocList); - }; - - return ( - <> - -
- - Upload documents - - - -
-

Electronic health records

- -
- - - - ); -}; - -export default SelectStage; diff --git a/app/src/components/blocks/_arf/uploadConfirmationFailed/uploadConfirmationFailed.test.tsx b/app/src/components/blocks/_arf/uploadConfirmationFailed/uploadConfirmationFailed.test.tsx deleted file mode 100644 index 38c40cfe8..000000000 --- a/app/src/components/blocks/_arf/uploadConfirmationFailed/uploadConfirmationFailed.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import UploadConfirmationFailed from './uploadConfirmationFailed'; -import { render, screen } from '@testing-library/react'; -import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; -import { describe, expect, it } from 'vitest'; - -describe('UploadConfirmationFailed', () => { - it('renders the page', () => { - render(); - - expect( - screen.getByRole('heading', { name: "We couldn't confirm the upload" }), - ).toBeInTheDocument(); - }); - - it('pass accessibility checks at page entry point', async () => { - render(); - - const results = await runAxeTest(document.body); - expect(results).toHaveNoViolations(); - }); -}); diff --git a/app/src/components/blocks/_arf/uploadConfirmationFailed/uploadConfirmationFailed.tsx b/app/src/components/blocks/_arf/uploadConfirmationFailed/uploadConfirmationFailed.tsx deleted file mode 100644 index 8143bcea4..000000000 --- a/app/src/components/blocks/_arf/uploadConfirmationFailed/uploadConfirmationFailed.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import useTitle from '../../../../helpers/hooks/useTitle'; - -const UploadConfirmationFailed = (): React.JSX.Element => { - const pageHeader = "We couldn't confirm the upload"; - useTitle({ pageTitle: pageHeader }); - - return ( - <> -

{pageHeader}

-

- The electronic health record was not uploaded for this patient. Please try uploading - the record again in a few minutes. -

-

- Make sure to safely store the electronic health record until it's completely - uploaded to this storage. -

- - ); -}; - -export default UploadConfirmationFailed; diff --git a/app/src/components/blocks/_arf/uploadFailedStage/uploadFailedStage.test.tsx b/app/src/components/blocks/_arf/uploadFailedStage/uploadFailedStage.test.tsx deleted file mode 100644 index b25bf0a7e..000000000 --- a/app/src/components/blocks/_arf/uploadFailedStage/uploadFailedStage.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; -import UploadFailedStage from './uploadFailedStage'; -import { describe, expect, it } from 'vitest'; - -describe('UploadFailedStage', () => { - it('renders the page', () => { - render(); - - expect( - screen.getByRole('heading', { name: 'All files failed to upload' }), - ).toBeInTheDocument(); - }); - - it('pass accessibility checks at page entry point', async () => { - render(); - - const results = await runAxeTest(document.body); - expect(results).toHaveNoViolations(); - }); -}); diff --git a/app/src/components/blocks/_arf/uploadFailedStage/uploadFailedStage.tsx b/app/src/components/blocks/_arf/uploadFailedStage/uploadFailedStage.tsx deleted file mode 100644 index 1025eeceb..000000000 --- a/app/src/components/blocks/_arf/uploadFailedStage/uploadFailedStage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import useTitle from '../../../../helpers/hooks/useTitle'; - -const UploadFailedStage = (): React.JSX.Element => { - const pageHeader = 'All files failed to upload'; - useTitle({ pageTitle: pageHeader }); - - return ( - <> -

{pageHeader}

-

- The electronic health record was not uploaded for this patient. You will need to - check your files and try again. -

-

- Make sure to safely store the electronic health record until it's completely - uploaded to this storage. -

- - ); -}; - -export default UploadFailedStage; diff --git a/app/src/components/blocks/_arf/uploadSummary/UploadSummary.test.tsx b/app/src/components/blocks/_arf/uploadSummary/UploadSummary.test.tsx deleted file mode 100644 index 937d4d722..000000000 --- a/app/src/components/blocks/_arf/uploadSummary/UploadSummary.test.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { render, screen, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import UploadSummary, { Props } from './UploadSummary'; -import { - DOCUMENT_UPLOAD_STATE as documentUploadStates, - UploadDocument, -} from '../../../../types/pages/UploadDocumentsPage/types'; -import { formatFileSize as formatSize } from '../../../../helpers/utils/formatFileSize'; -import { getFormattedDate } from '../../../../helpers/utils/formatDate'; -import { - buildDocument, - buildPatientDetails, - buildTextFile, -} from '../../../../helpers/test/testBuilders'; -import usePatient from '../../../../helpers/hooks/usePatient'; -import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; -import { afterEach, beforeEach, describe, expect, it, vi, Mock } from 'vitest'; - -vi.mock('../../../../helpers/hooks/usePatient'); -const mockedUsePatient = usePatient as Mock; -const mockPatient = buildPatientDetails(); - -describe('UploadSummary', () => { - beforeEach(() => { - import.meta.env.VITE_ENVIRONMENT = 'vitest'; - mockedUsePatient.mockReturnValue(mockPatient); - }); - afterEach(() => { - vi.clearAllMocks(); - }); - - it('renders the page', () => { - renderUploadSummary({ documents: [] }); - - expect(screen.getByRole('heading', { name: 'Upload Summary' })).toBeInTheDocument(); - expect( - screen.getByRole('heading', { - name: /All documents have been successfully uploaded on/, - }), - ).toBeInTheDocument(); - expect(screen.getByText('NHS number')).toBeInTheDocument(); - expect(screen.getByText('Surname')).toBeInTheDocument(); - expect(screen.getByText('First name')).toBeInTheDocument(); - expect(screen.getByText('Date of birth')).toBeInTheDocument(); - expect(screen.getByText('Postcode')).toBeInTheDocument(); - expect(screen.getByText('Before you close this page')).toBeInTheDocument(); - expect( - screen.queryByText('Some of your documents failed to upload'), - ).not.toBeInTheDocument(); - expect(screen.queryByText('View successfully uploaded documents')).not.toBeInTheDocument(); - }); - - it('displays successfully uploaded docs and callout message', () => { - const files = [buildTextFile('one', 100), buildTextFile('two', 101)]; - const documents = files.map((file) => buildDocument(file, documentUploadStates.SUCCEEDED)); - - renderUploadSummary({ documents }); - - expect( - screen.getByText(/All documents have been successfully uploaded on/), - ).toBeInTheDocument(); - expect(screen.getByText('View successfully uploaded documents')).toBeInTheDocument(); - const uploadedDocsTable = screen.getByRole('table', { - name: 'Successfully uploaded documents', - }); - files.forEach(({ name, size }) => { - expect(within(uploadedDocsTable).getByText(name)).toBeInTheDocument(); - expect(within(uploadedDocsTable).getByText(formatSize(size))).toBeInTheDocument(); - }); - expect(screen.getByText('Before you close this page')).toBeInTheDocument(); - }); - - it('displays a collapsible list of successfully uploaded docs', async () => { - const files = [buildTextFile('test1'), buildTextFile('test2')]; - const documents = files.map((file) => buildDocument(file, documentUploadStates.SUCCEEDED)); - - renderUploadSummary({ documents }); - - expect( - screen.queryByRole('table', { name: 'Successfully uploaded documents' }), - ).not.toBeVisible(); - - await userEvent.click(screen.getByText('View successfully uploaded documents')); - - expect( - screen.getByRole('table', { name: 'Successfully uploaded documents' }), - ).toBeVisible(); - - await userEvent.click(screen.getByText('View successfully uploaded documents')); - - expect( - screen.queryByRole('table', { name: 'Successfully uploaded documents' }), - ).not.toBeVisible(); - }); - - it('does not include docs that failed to upload in the successfully uploaded docs list', () => { - const uploadedFileName = 'one'; - const failedToUploadFileName = 'two'; - const documents = [ - buildDocument(buildTextFile(uploadedFileName, 100), documentUploadStates.SUCCEEDED), - buildDocument(buildTextFile(failedToUploadFileName, 101), documentUploadStates.FAILED), - ]; - - renderUploadSummary({ documents }); - - expect( - screen.queryByRole('heading', { - name: /All documents have been successfully uploaded on/, - }), - ).not.toBeInTheDocument(); - const uploadedDocsTable = screen.getByRole('table', { - name: 'Successfully uploaded documents', - }); - expect(within(uploadedDocsTable).getByText(`${uploadedFileName}.txt`)).toBeInTheDocument(); - expect( - within(uploadedDocsTable).queryByText(`${failedToUploadFileName}.txt`), - ).not.toBeInTheDocument(); - }); - - it('does not display the successfully uploads docs list when all of the docs failed to upload', () => { - const documents = [buildDocument(buildTextFile('test1'), documentUploadStates.FAILED)]; - - renderUploadSummary({ documents }); - - expect( - screen.queryByRole('heading', { - name: /All documents have been successfully uploaded on/, - }), - ).not.toBeInTheDocument(); - expect(screen.queryByText('View succe0ssfully uploaded documents')).not.toBeInTheDocument(); - }); - - it('displays message and does not display an alert if all the docs were uploaded successfully', () => { - const files = [buildTextFile('one', 100)]; - const documents = [buildDocument(files[0], documentUploadStates.SUCCEEDED)]; - - renderUploadSummary({ documents }); - - expect( - screen.getByRole('heading', { - name: - 'All documents have been successfully uploaded on ' + - getFormattedDate(new Date()), - }), - ).toBeInTheDocument(); - expect( - screen.queryByRole('alert', { - name: 'Some of your documents failed to upload', - }), - ).not.toBeInTheDocument(); - expect( - screen.queryByText(`${documents.length} of ${files.length} files failed to upload`), - ).not.toBeInTheDocument(); - }); - - it('displays an alert if some of the docs failed to upload', () => { - const documents = [ - buildDocument(buildTextFile('test1'), documentUploadStates.SUCCEEDED), - buildDocument(buildTextFile('test2'), documentUploadStates.FAILED), - ]; - - renderUploadSummary({ documents }); - - expect(screen.getByRole('alert', { name: 'There is a problem' })).toBeInTheDocument(); - expect( - screen.getByText( - 'Some documents failed to upload. You can try to upload the documents again if you wish, or they must be printed and sent via PCSE', - ), - ).toBeInTheDocument(); - expect(screen.getAllByText('Documents that have failed to upload')).toHaveLength(2); - expect( - screen.getByRole('link', { name: 'Documents that have failed to upload' }), - ).toHaveAttribute('href', '#failed-uploads'); - }); - - it('displays each doc that failed to upload in a table', () => { - const files = [buildTextFile('one', 100), buildTextFile('two', 101)]; - const documents = files.map((file) => buildDocument(file, documentUploadStates.FAILED)); - - renderUploadSummary({ documents }); - - const failedToUploadDocsTable = screen.getByRole('table', { - name: /failed to upload/, - }); - files.forEach(({ name, size }) => { - expect(within(failedToUploadDocsTable).getByText(name)).toBeInTheDocument(); - expect(within(failedToUploadDocsTable).getByText(formatSize(size))).toBeInTheDocument(); - }); - }); - - it('displays number of failed uploads and total uploads when there is at least 1 failed upload', () => { - const files = [buildTextFile('one', 100), buildTextFile('two', 101)]; - const documents: Array = files.map((file) => - buildDocument(file, documentUploadStates.FAILED), - ); - - renderUploadSummary({ documents }); - - expect( - screen.getByText(`${documents.length} of ${files.length} files failed to upload`), - ).toBeInTheDocument(); - }); - - describe('Accessibility', () => { - it('pass accessibility checks when upload result are all successful', async () => { - const files = [buildTextFile('one', 100), buildTextFile('two', 101)]; - const documents = files.map((file) => - buildDocument(file, documentUploadStates.SUCCEEDED), - ); - renderUploadSummary({ documents }); - - await screen.findByText(/All documents have been successfully uploaded/); - - const results = await runAxeTest(document.body); - expect(results).toHaveNoViolations(); - }); - - it('pass accessibility checks when result contain both successful and unsuccessful uploads', async () => { - const files = [buildTextFile('one', 100), buildTextFile('two', 101)]; - const documents = [ - buildDocument(files[0], documentUploadStates.FAILED), - buildDocument(files[1], documentUploadStates.SUCCEEDED), - ]; - renderUploadSummary({ documents }); - - await screen.findByText(/files failed to upload/); - await screen.findByText('View successfully uploaded documents'); - - const results = await runAxeTest(document.body); - expect(results).toHaveNoViolations(); - }); - }); -}); - -const renderUploadSummary = (propsOverride: Partial) => { - const props: Props = { - documents: [], - ...propsOverride, - }; - - render(); -}; diff --git a/app/src/components/blocks/_arf/uploadSummary/UploadSummary.tsx b/app/src/components/blocks/_arf/uploadSummary/UploadSummary.tsx deleted file mode 100644 index 4598b2fc3..000000000 --- a/app/src/components/blocks/_arf/uploadSummary/UploadSummary.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Details, Table, WarningCallout } from 'nhsuk-react-components'; -import React from 'react'; -import { - DOCUMENT_UPLOAD_STATE, - UploadDocument, -} from '../../../../types/pages/UploadDocumentsPage/types'; -import formatFileSize from '../../../../helpers/utils/formatFileSize'; -import { getFormattedDate } from '../../../../helpers/utils/formatDate'; -import ErrorBox from '../../../layout/errorBox/ErrorBox'; -import useTitle from '../../../../helpers/hooks/useTitle'; -import PatientSummary from '../../../generic/patientSummary/PatientSummary'; - -export interface Props { - documents: Array; -} -const UploadSummary = ({ documents }: Props): React.JSX.Element => { - const successfulUploads = documents.filter((document) => { - return document.state === DOCUMENT_UPLOAD_STATE.SUCCEEDED; - }); - - const failedUploads = documents.filter((document) => { - return [DOCUMENT_UPLOAD_STATE.FAILED, DOCUMENT_UPLOAD_STATE.INFECTED].includes( - document.state, - ); - }); - - const tableCaption = ( - <> -

- {failedUploads.length} of {documents.length} files failed to upload -

- - Error: Documents that have failed - to upload - - - ); - const pageHeader = 'Upload Summary'; - useTitle({ pageTitle: pageHeader }); - - return ( -
- {failedUploads.length > 0 && ( - - )} -

{pageHeader}

- {failedUploads.length > 0 && ( -
- - - {failedUploads.map((document) => { - return ( - - {document.file.name} - - {formatFileSize(document.file.size)} - - - ); - })} - -
-
- )} - {failedUploads.length === 0 && ( -

- All documents have been successfully uploaded on {getFormattedDate(new Date())} -

- )} - {successfulUploads.length > 0 && ( -
- - View successfully uploaded documents - - - - - - File Name - File Size - - - - {successfulUploads.map((document) => { - return ( - - {document.file.name} - - {formatFileSize(document.file.size)} - - - ); - })} - -
-
-
- )} - - - - Before you close this page -
    -
  • - You could take a screenshot of this summary page and attach it to the - patient's record -
  • -
  • - When you have finished uploading, and the patient is deducted from your - practice, delete all temporary files created for upload on your computer -
  • -
  • - If you have accidentally uploaded incorrect documents, please contact - Primary Care Support England (PSCE) -
  • -
-
-
- ); -}; - -export default UploadSummary; diff --git a/app/src/components/blocks/_arf/uploadingStage/UploadingStage.test.tsx b/app/src/components/blocks/_arf/uploadingStage/UploadingStage.test.tsx deleted file mode 100644 index 7d169edf7..000000000 --- a/app/src/components/blocks/_arf/uploadingStage/UploadingStage.test.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { - DOCUMENT_TYPE, - DOCUMENT_UPLOAD_STATE, - DOCUMENT_UPLOAD_STATE as documentUploadStates, - UploadDocument, -} from '../../../../types/pages/UploadDocumentsPage/types'; -import { buildTextFile } from '../../../../helpers/test/testBuilders'; -import UploadingStage from './UploadingStage'; -import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; -import { describe, expect, it } from 'vitest'; - -describe('', () => { - describe('with NHS number', () => { - const triggerUploadStateChange = ( - document: UploadDocument, - state: DOCUMENT_UPLOAD_STATE, - progress?: number, - ) => { - document.state = state; - document.progress = progress; - }; - - it('uploads documents and displays the progress', async () => { - const documentOne = { - file: buildTextFile('one', 100), - state: documentUploadStates.SELECTED, - id: '1', - progress: 0, - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - const documentTwo = { - file: buildTextFile('two', 200), - state: documentUploadStates.SELECTED, - id: '2', - progress: 0, - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - const documentThree = { - file: buildTextFile('three', 100), - state: documentUploadStates.SELECTED, - id: '3', - progress: 0, - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - render(); - - triggerUploadStateChange(documentOne, documentUploadStates.UPLOADING, 0); - - expect(screen.queryByTestId('upload-document-form')).not.toBeInTheDocument(); - expect( - screen.getByText( - 'Do not close or navigate away from this browser until upload is complete.', - ), - ).toBeInTheDocument(); - }); - - it('progress bar reflect the upload progress', async () => { - const documentOne = { - file: buildTextFile('one', 100), - state: documentUploadStates.SELECTED, - id: '1', - progress: 0, - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - const documentTwo = { - file: buildTextFile('two', 200), - state: documentUploadStates.SELECTED, - id: '2', - progress: 0, - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - const documentThree = { - file: buildTextFile('three', 100), - state: documentUploadStates.SELECTED, - id: '3', - progress: 0, - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - - const { rerender } = render( - , - ); - const getProgressBarValue = (document: UploadDocument) => { - const progressBar: HTMLProgressElement = screen.getByRole('progressbar', { - name: `Uploading ${document.file.name}`, - }); - return progressBar.value; - }; - const getProgressText = (document: UploadDocument) => { - return screen.getByRole('status', { - name: `${document.file.name} upload status`, - }).textContent; - }; - - triggerUploadStateChange(documentOne, documentUploadStates.UPLOADING, 10); - rerender(); - expect(getProgressBarValue(documentOne)).toEqual(10); - expect(getProgressText(documentOne)).toContain('10% uploaded...'); - - triggerUploadStateChange(documentOne, documentUploadStates.UPLOADING, 70); - rerender(); - expect(getProgressBarValue(documentOne)).toEqual(70); - expect(getProgressText(documentOne)).toContain('70% uploaded...'); - - triggerUploadStateChange(documentTwo, documentUploadStates.UPLOADING, 20); - rerender(); - expect(getProgressBarValue(documentTwo)).toEqual(20); - expect(getProgressText(documentTwo)).toContain('20% uploaded...'); - - triggerUploadStateChange(documentTwo, documentUploadStates.SUCCEEDED, 100); - rerender(); - expect(getProgressBarValue(documentTwo)).toEqual(100); - expect(getProgressText(documentTwo)).toContain('Upload succeeded'); - - triggerUploadStateChange(documentOne, documentUploadStates.FAILED, 0); - rerender(); - expect(getProgressBarValue(documentOne)).toEqual(0); - expect(getProgressText(documentOne)).toContain('Upload failed'); - - triggerUploadStateChange(documentTwo, documentUploadStates.SCANNING); - rerender(); - expect(getProgressText(documentTwo)).toContain('Virus scan in progress'); - - triggerUploadStateChange(documentTwo, documentUploadStates.CLEAN); - rerender(); - expect(getProgressText(documentTwo)).toContain('Virus scan complete'); - - triggerUploadStateChange(documentTwo, documentUploadStates.SUCCEEDED); - rerender(); - expect(getProgressText(documentTwo)).toContain('Upload succeeded'); - }); - }); - - it('pass accessibility check', async () => { - const documentOne = { - file: buildTextFile('one', 100), - state: documentUploadStates.UPLOADING, - id: '1', - progress: 10, - docType: DOCUMENT_TYPE.ARF, - attempts: 0, - }; - render(); - - await screen.findByText(documentOne.file.name); - - const results = await runAxeTest(document.body); - expect(results).toHaveNoViolations(); - }); -}); diff --git a/app/src/components/blocks/_arf/uploadingStage/UploadingStage.tsx b/app/src/components/blocks/_arf/uploadingStage/UploadingStage.tsx deleted file mode 100644 index 22244de3e..000000000 --- a/app/src/components/blocks/_arf/uploadingStage/UploadingStage.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { - DOCUMENT_UPLOAD_STATE, - UploadDocument, -} from '../../../../types/pages/UploadDocumentsPage/types'; -import { Table, WarningCallout } from 'nhsuk-react-components'; -import formatFileSize from '../../../../helpers/utils/formatFileSize'; -import useTitle from '../../../../helpers/hooks/useTitle'; -import { getUploadMessage } from '../../../../helpers/utils/uploadDocumentHelpers'; - -interface Props { - documents: Array; -} - -const UploadingStage = ({ documents }: Props): React.JSX.Element => { - const pageHeader = 'Your documents are uploading'; - useTitle({ pageTitle: 'Uploading documents' }); - - return ( - <> -

{pageHeader}

- - Stay on this page -

Do not close or navigate away from this browser until upload is complete.

-
- - - - File Name - File Size - File Upload Progress - - - - {documents.map((document) => { - const isScanning = document.state === DOCUMENT_UPLOAD_STATE.SCANNING; - return ( - - {document.file.name} - - {formatFileSize(document.file.size)} - - - - - {getUploadMessage(document)} - - - - ); - })} - -
- - ); -}; - -export default UploadingStage; diff --git a/app/src/components/blocks/_delete/deleteResultStage/DeleteResultStage.test.tsx b/app/src/components/blocks/_delete/deleteResultStage/DeleteResultStage.test.tsx index 27109902b..b53f0d95f 100644 --- a/app/src/components/blocks/_delete/deleteResultStage/DeleteResultStage.test.tsx +++ b/app/src/components/blocks/_delete/deleteResultStage/DeleteResultStage.test.tsx @@ -10,27 +10,34 @@ import usePatient from '../../../../helpers/hooks/usePatient'; import { DOWNLOAD_STAGE } from '../../../../types/generic/downloadStage'; import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; import { afterEach, beforeEach, describe, expect, it, vi, Mock } from 'vitest'; +import useConfig from '../../../../helpers/hooks/useConfig'; const mockNavigate = vi.fn(); vi.mock('../../../../helpers/hooks/useRole'); vi.mock('../../../../helpers/hooks/usePatient'); +vi.mock('../../../../helpers/hooks/useConfig'); vi.mock('react-router-dom', () => ({ - Link: (props: LinkProps) => , - useNavigate: () => mockNavigate, + Link: (props: LinkProps): React.JSX.Element => , + useNavigate: (): Mock => mockNavigate, })); const mockedUseRole = useRole as Mock; const mockedUsePatient = usePatient as Mock; +const mockedUseConfig = useConfig as Mock; const mockPatientDetails = buildPatientDetails(); -const mockLgSearchResult = buildLgSearchResult(); const mockSetDownloadStage = vi.fn(); describe('DeleteResultStage', () => { beforeEach(() => { import.meta.env.VITE_ENVIRONMENT = 'vitest'; mockedUsePatient.mockReturnValue(mockPatientDetails); + mockedUseConfig.mockReturnValue({ + featureFlags: { + uploadDocumentIteration3Enabled: true, + }, + }); }); afterEach(() => { vi.clearAllMocks(); @@ -41,19 +48,13 @@ describe('DeleteResultStage', () => { "renders the page with Lloyd George patient details when user role is '%s'", async (role) => { const patientName = `${mockPatientDetails.givenName} ${mockPatientDetails.familyName}`; - const numberOfFiles = mockLgSearchResult.numberOfFiles; mockedUseRole.mockReturnValue(role); - render( - , - ); + render(); await waitFor(async () => { expect( - screen.getByText('You have permanently removed the record of:'), + screen.getByText(`You have permanently removed the records of:`), ).toBeInTheDocument(); }); @@ -71,19 +72,13 @@ describe('DeleteResultStage', () => { ); it('renders the page with ARF patient details, when user role is PCSE', async () => { const patientName = `${mockPatientDetails.givenName} ${mockPatientDetails.familyName}`; - const numberOfFiles = 1; mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); - render( - , - ); + render(); await waitFor(async () => { expect( - screen.getByText('You have permanently removed the record of:'), + screen.getByText('You have permanently removed the records of:'), ).toBeInTheDocument(); }); @@ -97,19 +92,13 @@ describe('DeleteResultStage', () => { it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( "renders the return to Lloyd George Record button, when user role is '%s'", async (role) => { - const numberOfFiles = mockLgSearchResult.numberOfFiles; mockedUseRole.mockReturnValue(role); - render( - , - ); + render(); await waitFor(async () => { expect( - screen.getByText('You have permanently removed the record of:'), + screen.getByText('You have permanently removed the records of:'), ).toBeInTheDocument(); }); @@ -122,19 +111,13 @@ describe('DeleteResultStage', () => { ); it('does not render the return to Lloyd George Record button, when user role is PCSE', async () => { - const numberOfFiles = mockLgSearchResult.numberOfFiles; mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); - render( - , - ); + render(); await waitFor(async () => { expect( - screen.getByText('You have permanently removed the record of:'), + screen.getByText('You have permanently removed the records of:'), ).toBeInTheDocument(); }); @@ -146,19 +129,13 @@ describe('DeleteResultStage', () => { }); it('renders the Start Again button, when user role is PCSE', async () => { - const numberOfFiles = 7; mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); - render( - , - ); + render(); await waitFor(async () => { expect( - screen.getByText('You have permanently removed the record of:'), + screen.getByText('You have permanently removed the records of:'), ).toBeInTheDocument(); }); @@ -172,19 +149,13 @@ describe('DeleteResultStage', () => { it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( "does not render the Start Again button, when user role is '%s'", async (role) => { - const numberOfFiles = 7; mockedUseRole.mockReturnValue(role); - render( - , - ); + render(); await waitFor(async () => { expect( - screen.getByText('You have permanently removed the record of:'), + screen.getByText('You have permanently removed the records of:'), ).toBeInTheDocument(); }); @@ -201,7 +172,7 @@ describe('DeleteResultStage', () => { const roles = [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.PCSE]; it.each(roles)('pass accessibility checks for role %s', async (role) => { mockedUseRole.mockReturnValue(role); - render(); + render(); const results = await runAxeTest(document.body); expect(results).toHaveNoViolations(); @@ -212,19 +183,13 @@ describe('DeleteResultStage', () => { it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( "navigates to the Lloyd George view page when return button is clicked, when user role is '%s'", async (role) => { - const numberOfFiles = mockLgSearchResult.numberOfFiles; mockedUseRole.mockReturnValue(role); - render( - , - ); + render(); await waitFor(async () => { expect( - screen.getByText('You have permanently removed the record of:'), + screen.getByText('You have permanently removed the records of:'), ).toBeInTheDocument(); }); @@ -235,26 +200,20 @@ describe('DeleteResultStage', () => { ); await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith(routes.LLOYD_GEORGE); + expect(mockNavigate).toHaveBeenCalledWith(routes.PATIENT_DOCUMENTS); }); expect(mockSetDownloadStage).toHaveBeenCalledWith(DOWNLOAD_STAGE.REFRESH); }, ); it('navigates to Home page when link is clicked when user role is PCSE', async () => { - const numberOfFiles = 7; mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); - render( - , - ); + render(); await waitFor(async () => { expect( - screen.getByText('You have permanently removed the record of:'), + screen.getByText('You have permanently removed the records of:'), ).toBeInTheDocument(); }); diff --git a/app/src/components/blocks/_delete/deleteResultStage/DeleteResultStage.tsx b/app/src/components/blocks/_delete/deleteResultStage/DeleteResultStage.tsx index 5ee391c6f..c21de3137 100644 --- a/app/src/components/blocks/_delete/deleteResultStage/DeleteResultStage.tsx +++ b/app/src/components/blocks/_delete/deleteResultStage/DeleteResultStage.tsx @@ -7,26 +7,35 @@ import { REPOSITORY_ROLE } from '../../../../types/generic/authRole'; import ReducedPatientInfo from '../../../generic/reducedPatientInfo/ReducedPatientInfo'; import useTitle from '../../../../helpers/hooks/useTitle'; import { DOWNLOAD_STAGE } from '../../../../types/generic/downloadStage'; +import useConfig from '../../../../helpers/hooks/useConfig'; +import { DOCUMENT_TYPE, getDocumentTypeLabel } from '../../../../helpers/utils/documentType'; export type Props = { - numberOfFiles: number; + docType?: DOCUMENT_TYPE; setDownloadStage?: Dispatch>; }; -const DeleteResultStage = ({ numberOfFiles, setDownloadStage }: Props): React.JSX.Element => { +const DeleteResultStage = ({ docType, setDownloadStage }: Props): React.JSX.Element => { const navigate = useNavigate(); const role = useRole(); + const config = useConfig(); const handleClick = (e: MouseEvent): void => { e.preventDefault(); if (setDownloadStage) { setDownloadStage(DOWNLOAD_STAGE.REFRESH); } - navigate(routes.LLOYD_GEORGE); + navigate( + config.featureFlags.uploadDocumentIteration3Enabled + ? routes.PATIENT_DOCUMENTS + : routes.LLOYD_GEORGE, + ); }; + const recordLabel = docType ? getDocumentTypeLabel(docType) : ''; + const isGP = role === REPOSITORY_ROLE.GP_ADMIN || role === REPOSITORY_ROLE.GP_CLINICAL; - const pageHeader = 'You have permanently removed the record of:'; + const pageHeader = `You have permanently removed the ${recordLabel ? recordLabel : 'records'} of:`; useTitle({ pageTitle: pageHeader }); return ( diff --git a/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.test.tsx b/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.test.tsx index cd9ad5679..aa1c28778 100644 --- a/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.test.tsx +++ b/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.test.tsx @@ -3,7 +3,6 @@ import { buildLgSearchResult, buildPatientDetails } from '../../../../helpers/te import DeleteSubmitStage, { Props } from './DeleteSubmitStage'; import { getFormattedDate } from '../../../../helpers/utils/formatDate'; import userEvent from '@testing-library/user-event'; -import { DOCUMENT_TYPE } from '../../../../types/pages/UploadDocumentsPage/types'; import axios from 'axios'; import useRole from '../../../../helpers/hooks/useRole'; import { REPOSITORY_ROLE, authorisedRoles } from '../../../../types/generic/authRole'; @@ -15,6 +14,8 @@ import * as ReactRouter from 'react-router-dom'; import waitForSeconds from '../../../../helpers/utils/waitForSeconds'; import { afterEach, beforeEach, describe, expect, it, vi, Mock, Mocked } from 'vitest'; import { formatNhsNumber } from '../../../../helpers/utils/formatNhsNumber'; +import { DOCUMENT_TYPE } from '../../../../helpers/utils/documentType'; +import useConfig from '../../../../helpers/hooks/useConfig'; vi.mock('../../../../helpers/hooks/useConfig'); vi.mock('../../../../helpers/hooks/useBaseAPIHeaders'); @@ -28,10 +29,10 @@ vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, - useNavigate: () => mockedUseNavigate, + useNavigate: (): Mock => mockedUseNavigate, }; }); -Date.now = () => new Date('2020-01-01T00:00:00.000Z').getTime(); +Date.now = (): number => new Date('2020-01-01T00:00:00.000Z').getTime(); let history: MemoryHistory = createMemoryHistory({ initialEntries: ['/'], @@ -43,7 +44,7 @@ const mockedAxios = axios as Mocked; const mockedUsePatient = usePatient as Mock; const mockResetDocState = vi.fn(); const mockPatientDetails = buildPatientDetails(); -const mockLgSearchResult = buildLgSearchResult(); +const mockuseConfig = useConfig as Mock; const mockSetStage = vi.fn(); @@ -55,6 +56,11 @@ describe('DeleteSubmitStage', () => { }); import.meta.env.VITE_ENVIRONMENT = 'vitest'; mockedUsePatient.mockReturnValue(mockPatientDetails); + mockuseConfig.mockReturnValue({ + featureFlags: { + uploadDocumentIteration3Enabled: false, + }, + }); }); afterEach(() => { @@ -124,7 +130,7 @@ describe('DeleteSubmitStage', () => { await userEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith(routes.ARF_OVERVIEW); + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.PATIENT_DOCUMENTS); }); }); @@ -156,7 +162,7 @@ describe('DeleteSubmitStage', () => { await waitFor(() => { expect(mockedUseNavigate).toHaveBeenCalledWith( - routeChildren.LLOYD_GEORGE_DELETE_COMPLETE, + routeChildren.DOCUMENT_DELETE_COMPLETE, ); }); }); @@ -193,7 +199,9 @@ describe('DeleteSubmitStage', () => { await userEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith(routeChildren.ARF_DELETE_COMPLETE); + expect(mockedUseNavigate).toHaveBeenCalledWith( + routeChildren.DOCUMENT_DELETE_COMPLETE, + ); }); }); @@ -331,42 +339,40 @@ describe('DeleteSubmitStage', () => { expect(results).toHaveNoViolations(); }); }); -}); -describe('Navigation', () => { - it('navigates to session expire page when API call returns 403', async () => { - const errorResponse = { - response: { - status: 403, - message: 'Forbidden', - }, - }; - mockedAxios.delete.mockImplementation(() => Promise.reject(errorResponse)); - mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); + describe('Navigation', () => { + it('navigates to session expire page when API call returns 403', async () => { + const errorResponse = { + response: { + status: 403, + message: 'Forbidden', + }, + }; + mockedAxios.delete.mockImplementation(() => Promise.reject(errorResponse)); + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); - renderComponent(DOCUMENT_TYPE.ALL, history); + renderComponent(DOCUMENT_TYPE.ALL, history); - expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); - await userEvent.click(screen.getByRole('radio', { name: 'Yes' })); - await userEvent.click(screen.getByRole('button', { name: 'Continue' })); + await userEvent.click(screen.getByRole('radio', { name: 'Yes' })); + await userEvent.click(screen.getByRole('button', { name: 'Continue' })); - await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED); + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED); + }); }); }); }); -const renderComponent = (docType: DOCUMENT_TYPE, history: MemoryHistory) => { +const renderComponent = (docType: DOCUMENT_TYPE, history: MemoryHistory): void => { const props: Omit = { - numberOfFiles: mockLgSearchResult.numberOfFiles, docType, - recordType: docType.toString(), resetDocState: mockResetDocState, }; - return render( + render( , , diff --git a/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.tsx b/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.tsx index 3d397d0ec..75b0b5b7d 100644 --- a/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.tsx +++ b/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.tsx @@ -1,11 +1,10 @@ -import React, { Dispatch, SetStateAction, useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; import { FieldValues, useForm } from 'react-hook-form'; import { Button, Fieldset, Radios, WarningCallout } from 'nhsuk-react-components'; import deleteAllDocuments, { DeleteResponse, } from '../../../../helpers/requests/deleteAllDocuments'; import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders'; -import { DOCUMENT_TYPE } from '../../../../types/pages/UploadDocumentsPage/types'; import { DOWNLOAD_STAGE } from '../../../../types/generic/downloadStage'; import SpinnerButton from '../../../generic/spinnerButton/SpinnerButton'; import ServiceError from '../../../layout/serviceErrorBox/ServiceErrorBox'; @@ -22,16 +21,15 @@ import { isMock } from '../../../../helpers/utils/isLocal'; import useConfig from '../../../../helpers/hooks/useConfig'; import useTitle from '../../../../helpers/hooks/useTitle'; import ErrorBox from '../../../layout/errorBox/ErrorBox'; -import { getLastURLPath } from '../../../../helpers/utils/urlManipulations'; -import DeleteResultStage from '../deleteResultStage/DeleteResultStage'; import BackButton from '../../../generic/backButton/BackButton'; import PatientSummary, { PatientInfo } from '../../../generic/patientSummary/PatientSummary'; +import { DOCUMENT_TYPE, getDocumentTypeLabel } from '../../../../helpers/utils/documentType'; +import { getLastURLPath } from '../../../../helpers/utils/urlManipulations'; +import DeleteResultStage from '../deleteResultStage/DeleteResultStage'; export type Props = { docType: DOCUMENT_TYPE; - numberOfFiles: number; setDownloadStage?: Dispatch>; - recordType: string; resetDocState: () => void; }; @@ -42,13 +40,11 @@ enum DELETE_DOCUMENTS_OPTION { type IndexViewProps = { docType: DOCUMENT_TYPE; - recordType: string; resetDocState: () => void; }; const DeleteSubmitStageIndexView = ({ docType, - recordType, resetDocState, }: IndexViewProps): React.JSX.Element => { const patientDetails = usePatient(); @@ -70,11 +66,7 @@ const DeleteSubmitStageIndexView = ({ const onSuccess = (): void => { resetDocState(); setDeletionStage(SUBMISSION_STATE.SUCCEEDED); - if (userIsGP) { - navigate(routeChildren.LLOYD_GEORGE_DELETE_COMPLETE); - } else { - navigate(routeChildren.ARF_DELETE_COMPLETE); - } + navigate(routeChildren.DOCUMENT_DELETE_COMPLETE); }; try { setDeletionStage(SUBMISSION_STATE.PENDING); @@ -107,7 +99,7 @@ const DeleteSubmitStageIndexView = ({ if (role === REPOSITORY_ROLE.GP_ADMIN) { navigate(routes.LLOYD_GEORGE); } else if (role === REPOSITORY_ROLE.PCSE) { - navigate(routes.ARF_OVERVIEW); + navigate(routes.PATIENT_DOCUMENTS); } }; @@ -124,14 +116,16 @@ const DeleteSubmitStageIndexView = ({ } }; - const pageTitle = `You are removing the ${recordType} record of`; + const pageTitle = `You are removing the ${getDocumentTypeLabel(docType) ?? 'records'} of`; useTitle({ pageTitle }); return ( <> @@ -230,14 +224,9 @@ const DeleteSubmitStageIndexView = ({ const DeleteSubmitStage = ({ docType, - numberOfFiles, setDownloadStage, - recordType, resetDocState, }: Props): React.JSX.Element => { - const pageTitle = `You are removing the ${recordType} record of:`; - useTitle({ pageTitle }); - return ( <> @@ -246,34 +235,30 @@ const DeleteSubmitStage = ({ element={ } /> } - > + /> + } - > + /> ); }; + export default DeleteSubmitStage; diff --git a/app/src/components/blocks/_delete/removeRecordStage/RemoveRecordStage.test.tsx b/app/src/components/blocks/_delete/removeRecordStage/RemoveRecordStage.test.tsx index c7ee4eeda..628d402fd 100644 --- a/app/src/components/blocks/_delete/removeRecordStage/RemoveRecordStage.test.tsx +++ b/app/src/components/blocks/_delete/removeRecordStage/RemoveRecordStage.test.tsx @@ -8,17 +8,20 @@ import { MemoryHistory, createMemoryHistory } from 'history'; import * as ReactRouter from 'react-router-dom'; import waitForSeconds from '../../../../helpers/utils/waitForSeconds'; import { afterEach, beforeEach, describe, expect, it, vi, Mock, Mocked } from 'vitest'; +import { DOCUMENT_TYPE, getDocumentTypeLabel } from '../../../../helpers/utils/documentType'; +import useConfig from '../../../../helpers/hooks/useConfig'; vi.mock('axios'); -Date.now = () => new Date('2020-01-01T00:00:00.000Z').getTime(); +Date.now = (): number => new Date('2020-01-01T00:00:00.000Z').getTime(); vi.mock('react-router-dom', async () => ({ ...(await vi.importActual('react-router-dom')), - useNavigate: () => mockUseNavigate, + useNavigate: (): Mock => mockUseNavigate, })); vi.mock('../../../../helpers/hooks/useBaseAPIHeaders'); vi.mock('../../../../helpers/hooks/useBaseAPIUrl'); vi.mock('../../../../helpers/hooks/usePatient'); vi.mock('../../../../helpers/hooks/useRole'); +vi.mock('../../../../helpers/hooks/useConfig'); const mockedAxios = axios as Mocked; const mockUseNavigate = vi.fn(); @@ -26,10 +29,10 @@ const mockUsePatient = usePatient as Mock; const mockPatientDetails = buildPatientDetails(); const mockDownloadStage = vi.fn(); const mockResetDocState = vi.fn(); +const mockUseConfig = useConfig as Mock; const testFileName1 = 'John_1'; const testFileName2 = 'John_2'; -const numberOfFiles = 7; const searchResults = [ buildSearchResult({ fileName: testFileName1 }), buildSearchResult({ fileName: testFileName2 }), @@ -48,17 +51,26 @@ describe('RemoveRecordStage', () => { }); import.meta.env.VITE_ENVIRONMENT = 'vitest'; mockUsePatient.mockReturnValue(mockPatientDetails); + mockUseConfig.mockReturnValue({ + featureFlags: { + uploadDocumentIteration3Enabled: true, + }, + }); }); afterEach(() => { vi.clearAllMocks(); }); describe('Render', () => { - it('renders the component', () => { + it.each([ + DOCUMENT_TYPE.LLOYD_GEORGE, + DOCUMENT_TYPE.EHR, + DOCUMENT_TYPE.EHR_ATTACHMENTS, + DOCUMENT_TYPE.LETTERS_AND_DOCS, + ])('renders the component for %s', (docType) => { mockedAxios.get.mockImplementation(() => waitForSeconds(0)); - const recordType = 'Test Record'; - renderComponent(history, numberOfFiles, recordType); + renderComponent(history, docType); expect( - screen.getByRole('heading', { name: 'Remove this ' + recordType + ' record' }), + screen.getByRole('heading', { name: 'Remove ' + getDocumentTypeLabel(docType) }), ).toBeInTheDocument(); expect( screen.getByText( @@ -70,13 +82,13 @@ describe('RemoveRecordStage', () => { it('show progress bar when file search pending', () => { mockedAxios.get.mockImplementation(() => waitForSeconds(0)); - const recordType = 'Test Record'; - renderComponent(history, numberOfFiles, recordType); + const recordType = DOCUMENT_TYPE.LLOYD_GEORGE; + renderComponent(history, recordType); expect(screen.getByRole('progressbar', { name: 'Loading...' })).toBeInTheDocument(); }); it('show service error when file search failed', async () => { - const recordType = 'Test Record'; + const recordType = DOCUMENT_TYPE.LLOYD_GEORGE; const errorResponse = { response: { status: 400, @@ -84,7 +96,7 @@ describe('RemoveRecordStage', () => { }, }; mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); - renderComponent(history, numberOfFiles, recordType); + renderComponent(history, recordType); expect(screen.getByRole('progressbar', { name: 'Loading...' })).toBeInTheDocument(); await waitFor(() => { expect( @@ -97,9 +109,9 @@ describe('RemoveRecordStage', () => { }); it('show results when when file search succeeded', async () => { - const recordType = 'Test Record'; + const recordType = DOCUMENT_TYPE.LLOYD_GEORGE; mockedAxios.get.mockImplementation(() => Promise.resolve({ data: searchResults })); - renderComponent(history, numberOfFiles, recordType); + renderComponent(history, recordType); expect(screen.getByRole('progressbar', { name: 'Loading...' })).toBeInTheDocument(); await waitFor(() => { expect( @@ -114,7 +126,7 @@ describe('RemoveRecordStage', () => { describe('Navigate', () => { it('navigates to server error page when search 500', async () => { - const recordType = 'Test Record'; + const recordType = DOCUMENT_TYPE.LLOYD_GEORGE; const errorResponse = { response: { status: 500, @@ -122,7 +134,7 @@ describe('RemoveRecordStage', () => { }, }; mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); - renderComponent(history, numberOfFiles, recordType); + renderComponent(history, recordType); expect(screen.getByRole('progressbar', { name: 'Loading...' })).toBeInTheDocument(); const mockedShortcode = '?encodedError=WyJTUF8xMDAxIiwiMTU3NzgzNjgwMCJd'; await waitFor(() => { @@ -131,7 +143,7 @@ describe('RemoveRecordStage', () => { }); it('navigates to session expired page when search 403', async () => { - const recordType = 'Test Record'; + const recordType = DOCUMENT_TYPE.LLOYD_GEORGE; const errorResponse = { response: { status: 403, @@ -139,7 +151,7 @@ describe('RemoveRecordStage', () => { }, }; mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); - renderComponent(history, numberOfFiles, recordType); + renderComponent(history, recordType); expect(screen.getByRole('progressbar', { name: 'Loading...' })).toBeInTheDocument(); await waitFor(() => { expect(mockUseNavigate).toBeCalledWith(routes.SESSION_EXPIRED); @@ -148,12 +160,11 @@ describe('RemoveRecordStage', () => { }); }); -const renderComponent = (history: MemoryHistory, numberOfFiles: number, recordType: string) => { - return render( +const renderComponent = (history: MemoryHistory, recordType: DOCUMENT_TYPE): void => { + render( diff --git a/app/src/components/blocks/_delete/removeRecordStage/RemoveRecordStage.tsx b/app/src/components/blocks/_delete/removeRecordStage/RemoveRecordStage.tsx index 8248610c2..8b0fb660a 100644 --- a/app/src/components/blocks/_delete/removeRecordStage/RemoveRecordStage.tsx +++ b/app/src/components/blocks/_delete/removeRecordStage/RemoveRecordStage.tsx @@ -18,30 +18,30 @@ import { isMock } from '../../../../helpers/utils/isLocal'; import { buildSearchResult } from '../../../../helpers/test/testBuilders'; import { getLastURLPath } from '../../../../helpers/utils/urlManipulations'; import DeleteSubmitStage from '../deleteSubmitStage/DeleteSubmitStage'; -import { DOCUMENT_TYPE } from '../../../../types/pages/UploadDocumentsPage/types'; import DeleteResultStage from '../deleteResultStage/DeleteResultStage'; import { DOWNLOAD_STAGE } from '../../../../types/generic/downloadStage'; import PatientSummary from '../../../generic/patientSummary/PatientSummary'; import BackButton from '../../../generic/backButton/BackButton'; import useRole from '../../../../helpers/hooks/useRole'; import { REPOSITORY_ROLE } from '../../../../types/generic/authRole'; +import { DOCUMENT_TYPE, getDocumentTypeLabel } from '../../../../helpers/utils/documentType'; +import useConfig from '../../../../helpers/hooks/useConfig'; export type Props = { - numberOfFiles: number; - recordType: string; + docType: DOCUMENT_TYPE; setDownloadStage: Dispatch>; resetDocState: () => void; }; const RemoveRecordStage = ({ - numberOfFiles, - recordType, + docType, setDownloadStage, resetDocState, }: Props): React.JSX.Element => { useTitle({ pageTitle: 'Remove record' }); const patientDetails = usePatient(); const [submissionState, setSubmissionState] = useState(SUBMISSION_STATE.PENDING); + const config = useConfig(); const role = useRole(); @@ -65,6 +65,7 @@ const RemoveRecordStage = ({ nhsNumber, baseUrl, baseHeaders, + docType, }); onSuccess(results ?? []); } catch (e) { @@ -97,10 +98,24 @@ const RemoveRecordStage = ({ const hasDocuments = !!searchResults.length && !!patientDetails; + const pageHeader = (): string => { + if (config.featureFlags.uploadDocumentIteration3Enabled) { + return getDocumentTypeLabel(docType) || 'patient record'; + } + + return 'Lloyd George record'; + }; + + const getDeleteConfirmationPath = (): string => { + return config.featureFlags.uploadDocumentIteration3Enabled + ? routeChildren.DOCUMENT_DELETE_CONFIRMATION + : routeChildren.LLOYD_GEORGE_DELETE_CONFIRMATION; + }; + const PageIndexView = (): React.JSX.Element => ( <> -

Remove this {recordType} record

+

Remove {pageHeader()}

{role !== REPOSITORY_ROLE.PCSE && ( @@ -131,7 +146,7 @@ const RemoveRecordStage = ({ <> {hasDocuments ? ( <> - +
Filename @@ -172,7 +187,7 @@ const RemoveRecordStage = ({
+ + + Document type + Filename + Date uploaded + View + + + + {props.searchResults?.map((result, index) => ( + + + {getDocumentTypeLabel(result.documentSnomedCodeType) ?? 'Other'} + + + {result.fileName} + + + {getFormattedDate(new Date(result.created))} + + + {canViewFiles && props.onViewDocument && ( + props.onViewDocument!(result)} + id={`available-files-row-${index}-view-link`} + data-testid={`view-${index}-link`} + href="#" + > + View + + )} + + + ))} + +
+ + + ); +}; + +export default DocumentSearchResults; diff --git a/app/src/components/blocks/_arf/documentSearchResultsOptions/DocumentSearchResultsOptions.test.tsx b/app/src/components/blocks/_patientDocuments/documentSearchResultsOptions/DocumentSearchResultsOptions.test.tsx similarity index 98% rename from app/src/components/blocks/_arf/documentSearchResultsOptions/DocumentSearchResultsOptions.test.tsx rename to app/src/components/blocks/_patientDocuments/documentSearchResultsOptions/DocumentSearchResultsOptions.test.tsx index 29a9fc8bf..debeaad64 100644 --- a/app/src/components/blocks/_arf/documentSearchResultsOptions/DocumentSearchResultsOptions.test.tsx +++ b/app/src/components/blocks/_patientDocuments/documentSearchResultsOptions/DocumentSearchResultsOptions.test.tsx @@ -127,9 +127,7 @@ describe('DocumentSearchResultsOptions', () => { await userEvent.click(screen.getByRole('button', { name: 'Remove all documents' })); await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith( - routeChildren.ARF_DELETE_CONFIRMATION, - ); + expect(mockedUseNavigate).toHaveBeenCalledWith(routeChildren.DOCUMENT_DELETE); }); }); }); diff --git a/app/src/components/blocks/_arf/documentSearchResultsOptions/DocumentSearchResultsOptions.tsx b/app/src/components/blocks/_patientDocuments/documentSearchResultsOptions/DocumentSearchResultsOptions.tsx similarity index 92% rename from app/src/components/blocks/_arf/documentSearchResultsOptions/DocumentSearchResultsOptions.tsx rename to app/src/components/blocks/_patientDocuments/documentSearchResultsOptions/DocumentSearchResultsOptions.tsx index d6ae7de9f..0053eb460 100644 --- a/app/src/components/blocks/_arf/documentSearchResultsOptions/DocumentSearchResultsOptions.tsx +++ b/app/src/components/blocks/_patientDocuments/documentSearchResultsOptions/DocumentSearchResultsOptions.tsx @@ -7,9 +7,9 @@ import getPresignedUrlForZip from '../../../../helpers/requests/getPresignedUrlF import { AxiosError } from 'axios'; import { useEffect, useRef, useState } from 'react'; import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders'; -import { DOCUMENT_TYPE } from '../../../../types/pages/UploadDocumentsPage/types'; import useBaseAPIUrl from '../../../../helpers/hooks/useBaseAPIUrl'; import { errorToParams } from '../../../../helpers/utils/errorToParams'; +import { DOCUMENT_TYPE } from '../../../../helpers/utils/documentType'; type Props = { nhsNumber: string; @@ -81,18 +81,13 @@ const DocumentSearchResultsOptions = (props: Props): React.JSX.Element => { }; const deleteAllDocuments = (): void => { - navigate(routeChildren.ARF_DELETE_CONFIRMATION); + navigate(routeChildren.DOCUMENT_DELETE); }; return ( <> -
- {statusMessage} +
+ {statusMessage}
{props.downloadState === SUBMISSION_STATE.PENDING ? ( @@ -102,7 +97,7 @@ const DocumentSearchResultsOptions = (props: Props): React.JSX.Element => { disabled={true} /> ) : ( - )} diff --git a/app/src/components/blocks/_patientDocuments/documentView/DocumentView.test.tsx b/app/src/components/blocks/_patientDocuments/documentView/DocumentView.test.tsx new file mode 100644 index 000000000..b310431dc --- /dev/null +++ b/app/src/components/blocks/_patientDocuments/documentView/DocumentView.test.tsx @@ -0,0 +1,308 @@ +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; +import DocumentView from './DocumentView'; +import usePatient from '../../../../helpers/hooks/usePatient'; +import useTitle from '../../../../helpers/hooks/useTitle'; +import { DOCUMENT_TYPE, getDocumentTypeLabel } from '../../../../helpers/utils/documentType'; +import { DocumentReference } from '../../../../types/pages/documentSearchResultsPage/types'; +import { routes } from '../../../../types/generic/routes'; +import { buildPatientDetails } from '../../../../helpers/test/testBuilders'; +import userEvent from '@testing-library/user-event'; +import { getFormattedDate } from '../../../../helpers/utils/formatDate'; +import { lloydGeorgeRecordLinks } from '../../../../types/blocks/lloydGeorgeActions'; +import SessionProvider from '../../../../providers/sessionProvider/SessionProvider'; +import { createMemoryHistory } from 'history'; +import * as ReactRouter from 'react-router-dom'; +import { REPOSITORY_ROLE } from '../../../../types/generic/authRole'; +import useRole from '../../../../helpers/hooks/useRole'; + +// Mock dependencies +vi.mock('../../../../helpers/hooks/usePatient'); +vi.mock('../../../../helpers/hooks/useTitle'); +vi.mock('../../../../helpers/hooks/useRole'); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockUseNavigate, + createSearchParams: () => mockCreateSearchParams, + }; +}); + +const mockUsePatient = usePatient as Mock; +const mockUseTitle = useTitle as Mock; +const mockUseRole = useRole as Mock; +const mockUseNavigate = vi.fn(); +const mockRemoveDocuments = vi.fn(); +const mockCreateSearchParams = vi.fn(); + +const EMBEDDED_PDF_VIEWER_TITLE = 'Embedded PDF Viewer'; + +const mockDocumentReference: DocumentReference = { + id: 'test-id', + fileName: 'test-document.pdf', + created: '2023-01-01T10:00:00Z', + url: 'https://example.com/document.pdf', + contentType: 'application/pdf', + documentSnomedCodeType: DOCUMENT_TYPE.LLOYD_GEORGE, + version: '1', + virusScannerResult: 'clean', + fileSize: 1024, + isPdf: true, +}; + +const mockPatientDetails = buildPatientDetails(); + +const simulateFullscreenChange = (isFullscreen: boolean) => { + act(() => { + // Update the fullscreenElement property to simulate browser state + Object.defineProperty(document, 'fullscreenElement', { + writable: true, + configurable: true, + value: isFullscreen ? document.documentElement : null, + }); + + // Dispatch the fullscreenchange event + document.dispatchEvent(new Event('fullscreenchange')); + }); +}; + +type Props = { + documentReference: DocumentReference | null; +}; + +const TestApp = ({ documentReference }: Props) => { + const history = createMemoryHistory(); + return ( + + + + ); +}; + +const renderComponent = (documentReference: DocumentReference | null = mockDocumentReference) => { + render( + + + , + ); +}; + +describe('DocumentView', () => { + beforeEach(() => { + import.meta.env.VITE_ENVIRONMENT = 'vitest'; + mockUsePatient.mockReturnValue(mockPatientDetails); + mockUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); + + // Mock fullscreen API + Object.defineProperty(document, 'fullscreenEnabled', { + writable: true, + configurable: true, + value: true, + }); + + Object.defineProperty(document, 'fullscreenElement', { + writable: true, + configurable: true, + value: null, + }); + + // Mock fetch + global.fetch = vi.fn().mockResolvedValue({ + blob: () => Promise.resolve(new Blob()), + }); + }); + + describe('Component rendering', () => { + it('renders the document view with all components', () => { + renderComponent(); + + expect(screen.getByText('Lloyd George records')).toBeInTheDocument(); + expect(screen.getByTestId('patient-summary')).toBeInTheDocument(); + expect(screen.getByTestId('go-back-button')).toBeInTheDocument(); + }); + + it('sets the page title correctly', () => { + renderComponent(); + + expect(mockUseTitle).toHaveBeenCalledWith({ pageTitle: 'Lloyd George records' }); + }); + + it('navigates to patient documents when documentReference is null', () => { + renderComponent(null); + + expect(mockUseNavigate).toHaveBeenCalledWith(routes.PATIENT_DOCUMENTS); + }); + }); + + describe('Fullscreen mode', () => { + it('shows full screen mode with patient info', async () => { + const patientName = `${mockPatientDetails.familyName}, ${mockPatientDetails.givenName}`; + const dob = getFormattedDate(new Date(mockPatientDetails.birthDate)); + + renderComponent(); + + await screen.findByTitle(EMBEDDED_PDF_VIEWER_TITLE); + await userEvent.click(screen.getByText('View in full screen')); + + // Simulate the browser entering fullscreen + simulateFullscreenChange(true); + + await screen.findByText('Exit full screen'); + + expect(screen.getByText(patientName)).toBeInTheDocument(); + expect(screen.getByText(new RegExp(dob))).toBeInTheDocument(); + expect(screen.getByText(/NHS number/)).toBeInTheDocument(); + }); + + it('shows deceased tag for deceased patients', async () => { + mockUsePatient.mockReturnValue(buildPatientDetails({ deceased: true })); + renderComponent(); + + expect(screen.getByTestId('deceased-patient-tag')).toBeInTheDocument(); + }); + + it('returns to regular view when exiting full screen', async () => { + renderComponent(); + + await userEvent.click(await screen.findByText('View in full screen')); + // Simulate entering fullscreen + simulateFullscreenChange(true); + + await userEvent.click(await screen.findByText('Exit full screen')); + // Simulate exiting fullscreen + simulateFullscreenChange(false); + + expect(screen.getByText('View in full screen')).toBeInTheDocument(); + }); + }); + + describe('Document details', () => { + it('displays formatted creation date', () => { + renderComponent(); + + expect(screen.getByText(/Last updated:/)).toBeInTheDocument(); + }); + + it.each( + Array.from(Object.values(DOCUMENT_TYPE)).filter((type) => type !== DOCUMENT_TYPE.ALL), + )('displays document type label in record card when doc type is %s', (documentType) => { + renderComponent({ + ...mockDocumentReference, + documentSnomedCodeType: documentType, + }); + + expect(screen.getByTestId('record-card-container')).toHaveTextContent( + getDocumentTypeLabel(documentType), + ); + }); + }); + + describe('Add Files functionality', () => { + it('shows add files section for Lloyd George documents when not in fullscreen', () => { + renderComponent(); + + expect(screen.getByTestId('add-files-btn')).toBeInTheDocument(); + }); + + it('does not show add files section when in fullscreen', async () => { + renderComponent(); + + await screen.findByTitle(EMBEDDED_PDF_VIEWER_TITLE); + await userEvent.click(screen.getByText('View in full screen')); + + // Simulate the browser entering fullscreen + simulateFullscreenChange(true); + + expect(screen.queryByText('Add Files')).not.toBeInTheDocument(); + }); + + it('does not show add files section for non-Lloyd George documents', () => { + const nonLGDocument = { + ...mockDocumentReference, + documentSnomedCodeType: DOCUMENT_TYPE.EHR, + }; + + renderComponent(nonLGDocument); + + expect(screen.queryByText('Add Files')).not.toBeInTheDocument(); + }); + + it('navigates to upload page when add files is clicked', async () => { + renderComponent(); + + const addFilesButton = screen.getByTestId('add-files-btn'); + fireEvent.click(addFilesButton); + + await waitFor(() => { + expect(mockUseNavigate).toHaveBeenCalledWith( + expect.objectContaining({ + pathname: routes.DOCUMENT_UPLOAD, + }), + expect.objectContaining({ + state: expect.objectContaining({ + journey: 'update', + existingDocuments: expect.arrayContaining([ + expect.objectContaining({ + fileName: mockDocumentReference.fileName, + }), + ]), + }), + }), + ); + }); + }); + + it('navigates to server error when patient has no NHS number', async () => { + mockUsePatient.mockReturnValue({ nhsNumber: null }); + + renderComponent(); + + const addFilesButton = screen.getByTestId('add-files-btn'); + fireEvent.click(addFilesButton); + + await waitFor(() => { + expect(mockUseNavigate).toHaveBeenCalledWith(routes.SERVER_ERROR); + }); + }); + }); + + describe('Document actions', () => { + it('calls removeDocuments when remove action is triggered', () => { + renderComponent(); + + // Assuming the first record link is remove action + const removeRecordLink = screen.getByTestId(lloydGeorgeRecordLinks[0].key); + fireEvent.click(removeRecordLink); + expect(mockRemoveDocuments).toHaveBeenCalledWith( + mockDocumentReference.documentSnomedCodeType, + ); + }); + }); + + describe('Role-based rendering', () => { + it('shows menu for GP_ADMIN role when not in fullscreen', () => { + renderComponent(); + + // Menu should be available for GP_ADMIN + expect(screen.getByTestId('record-menu-card')).toBeInTheDocument(); + }); + + it('does not show menu when in fullscreen mode', async () => { + renderComponent(); + + await screen.findByTitle(EMBEDDED_PDF_VIEWER_TITLE); + await userEvent.click(screen.getByText('View in full screen')); + + // Simulate the browser entering fullscreen + simulateFullscreenChange(true); + + // Check that fullscreen layout is different + expect(screen.getByText('Exit full screen')).toBeInTheDocument(); + expect(screen.queryByTestId('record-menu-card')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/app/src/components/blocks/_patientDocuments/documentView/DocumentView.tsx b/app/src/components/blocks/_patientDocuments/documentView/DocumentView.tsx new file mode 100644 index 000000000..39d02de23 --- /dev/null +++ b/app/src/components/blocks/_patientDocuments/documentView/DocumentView.tsx @@ -0,0 +1,262 @@ +import { routeChildren, routes } from '../../../../types/generic/routes'; +import useTitle from '../../../../helpers/hooks/useTitle'; +import { useSessionContext } from '../../../../providers/sessionProvider/SessionProvider'; +import { DOCUMENT_TYPE, getDocumentTypeLabel } from '../../../../helpers/utils/documentType'; +import { getFormattedDate } from '../../../../helpers/utils/formatDate'; +import { DocumentReference } from '../../../../types/pages/documentSearchResultsPage/types'; +import { + getRecordActionLinksAllowedForRole, + LGRecordActionLink, + lloydGeorgeRecordLinks, + RECORD_ACTION, +} from '../../../../types/blocks/lloydGeorgeActions'; +import { createSearchParams, NavigateOptions, To, useNavigate } from 'react-router-dom'; +import { REPOSITORY_ROLE } from '../../../../types/generic/authRole'; +import RecordCard from '../../../generic/recordCard/RecordCard'; +import PatientSummary, { PatientInfo } from '../../../generic/patientSummary/PatientSummary'; +import RecordMenuCard from '../../../generic/recordMenuCard/RecordMenuCard'; +import { Button, ChevronLeftIcon } from 'nhsuk-react-components'; +import BackButton from '../../../generic/backButton/BackButton'; +import usePatient from '../../../../helpers/hooks/usePatient'; +import { useEffect } from 'react'; +import useRole from '../../../../helpers/hooks/useRole'; + +type Props = { + documentReference: DocumentReference | null; + removeDocuments: (docType: DOCUMENT_TYPE) => void; +}; + +const DocumentView = ({ + documentReference, + removeDocuments, +}: Readonly): React.JSX.Element => { + const [session, setUserSession] = useSessionContext(); + const role = useRole(); + const navigate = useNavigate(); + const showMenu = role === REPOSITORY_ROLE.GP_ADMIN && !session.isFullscreen; + const patientDetails = usePatient(); + + const pageHeader = 'Lloyd George records'; + useTitle({ pageTitle: pageHeader }); + + // Handle fullscreen changes from browser events + useEffect(() => { + const handleFullscreenChange = (): void => { + const isCurrentlyFullscreen = document.fullscreenElement !== null; + // Only update if the state has actually changed to avoid unnecessary re-renders + if (session.isFullscreen !== isCurrentlyFullscreen) { + setUserSession({ + ...session, + isFullscreen: isCurrentlyFullscreen, + }); + } + }; + + document.addEventListener('fullscreenchange', handleFullscreenChange); + + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange); + }; + }, [session, setUserSession]); + + if (!documentReference) { + navigate(routes.PATIENT_DOCUMENTS); + return <>; + } + + const details = (): React.JSX.Element => { + return ( +
+
+
+ Filename: {documentReference.fileName} +
+
+ Last updated: {getFormattedDate(new Date(documentReference.created))} +
+
+
+ ); + }; + + const downloadClicked = (): void => { + if (documentReference.url) { + const anchor = document.createElement('a'); + anchor.href = documentReference.url; + anchor.download = documentReference.fileName; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + } + }; + + const removeClicked = (): void => { + disableFullscreen(); + removeDocuments(documentReference.documentSnomedCodeType); + }; + + const getCardLinks = (): Array => { + if (session.isFullscreen) { + return []; + } + + const links = getRecordActionLinksAllowedForRole({ + role, + hasRecordInStorage: true, + inputLinks: lloydGeorgeRecordLinks, + }); + + return links.map((link) => { + return { + ...link, + href: + link.type === RECORD_ACTION.DELETE ? routeChildren.DOCUMENT_DELETE : undefined, + onClick: link.type === RECORD_ACTION.DOWNLOAD ? downloadClicked : removeClicked, + }; + }); + }; + + const getPdfObjectUrl = (): string => { + if (documentReference.contentType !== 'application/pdf') { + return ''; + } + + return documentReference.url ? documentReference.url : 'loading'; + }; + + const enableFullscreen = (): void => { + if (document.fullscreenEnabled) { + document.documentElement.requestFullscreen?.(); + } + }; + + const disableFullscreen = (): void => { + if (document.fullscreenElement !== null) { + document.exitFullscreen?.(); + } + }; + + const handleAddFilesClick = async (): Promise => { + if (!patientDetails?.nhsNumber) { + navigate(routes.SERVER_ERROR); + return; + } + + const fileName = documentReference.fileName; + const documentId = documentReference.id; + const versionId = documentReference.version; + + const response = await fetch(documentReference.url!); + const blob = await response.blob(); + + const to: To = { + pathname: routes.DOCUMENT_UPLOAD, + search: createSearchParams({ journey: 'update' }).toString(), + }; + const options: NavigateOptions = { + state: { + journey: 'update', + existingDocuments: [{ fileName, blob, documentId, versionId }], + }, + }; + navigate(to, options); + }; + + const getRecordCard = (): React.JSX.Element => { + const card = ( + + ); + return session.isFullscreen ? ( + card + ) : ( +
+
+ {card} +
+
+ ); + }; + + return ( +
+ {session.isFullscreen && ( +
+
+ +

{pageHeader}

+ + Sign out + +
+
+ )} + +
+
+ {!session.isFullscreen && ( + <> + +

{pageHeader}

+ + )} + + + + + + + + {session.isFullscreen && ( + {}} + showMenu={showMenu} + /> + )} + + {!session.isFullscreen && + documentReference.documentSnomedCodeType === DOCUMENT_TYPE.LLOYD_GEORGE && ( + <> +

Add Files

+

You can add more files to this patient's record.

+ + + )} +
+ + {getRecordCard()} +
+
+ ); +}; + +export default DocumentView; diff --git a/app/src/components/generic/recordCard/RecordCard.test.tsx b/app/src/components/generic/recordCard/RecordCard.test.tsx index b1c608cbb..3da59d032 100644 --- a/app/src/components/generic/recordCard/RecordCard.test.tsx +++ b/app/src/components/generic/recordCard/RecordCard.test.tsx @@ -152,7 +152,7 @@ describe('RecordCard Component', () => { await userEvent.click(screen.getByTestId('full-screen-btn')); - expect(mockFullScreenHandler).toHaveBeenCalledWith(true); + expect(mockFullScreenHandler).toHaveBeenCalled(); }); }); }); diff --git a/app/src/components/generic/recordCard/RecordCard.tsx b/app/src/components/generic/recordCard/RecordCard.tsx index 64f972320..bd6f6d053 100644 --- a/app/src/components/generic/recordCard/RecordCard.tsx +++ b/app/src/components/generic/recordCard/RecordCard.tsx @@ -4,10 +4,11 @@ import PdfViewer from '../pdfViewer/PdfViewer'; import { LGRecordActionLink } from '../../../types/blocks/lloydGeorgeActions'; import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; import RecordMenuCard from '../recordMenuCard/RecordMenuCard'; +import Spinner from '../spinner/Spinner'; export type Props = { heading: string; - fullScreenHandler: (clicked: true) => void; + fullScreenHandler: () => void; detailsElement: ReactNode; isFullScreen: boolean; pdfObjectUrl: string; @@ -27,7 +28,18 @@ const RecordCard = ({ showMenu = false, }: Props): React.JSX.Element => { const Record = (): React.JSX.Element => { - return pdfObjectUrl ? : <>; + switch (pdfObjectUrl) { + case '': + case null: + case undefined: + return <>; + + case 'loading': + return ; + + default: + return ; + } }; const RecordLayout = ({ children }: { children: ReactNode }): React.JSX.Element => { @@ -56,9 +68,7 @@ const RecordCard = ({ diff --git a/app/src/components/generic/recordMenuCard/RecordMenuCard.tsx b/app/src/components/generic/recordMenuCard/RecordMenuCard.tsx index dd91d27b2..6acd14ee6 100644 --- a/app/src/components/generic/recordMenuCard/RecordMenuCard.tsx +++ b/app/src/components/generic/recordMenuCard/RecordMenuCard.tsx @@ -60,7 +60,13 @@ const LinkSection = ({ actionLinks, setStage }: SubSectionProps): React.JSX.Elem return ( <> {actionLinks.map((link) => ( - + ))} ); diff --git a/app/src/helpers/requests/deleteAllDocuments.test.ts b/app/src/helpers/requests/deleteAllDocuments.test.ts index 00c3301ce..3c5d40da9 100644 --- a/app/src/helpers/requests/deleteAllDocuments.test.ts +++ b/app/src/helpers/requests/deleteAllDocuments.test.ts @@ -1,7 +1,7 @@ import axios, { AxiosError } from 'axios'; import deleteAllDocuments, { DeleteResponse } from './deleteAllDocuments'; -import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types'; import { describe, expect, test, vi, Mocked } from 'vitest'; +import { DOCUMENT_TYPE } from '../utils/documentType'; vi.mock('axios'); const mockedAxios = axios as Mocked; @@ -12,7 +12,7 @@ describe('[DELETE] deleteAllDocuments', () => { test('Delete all documents handles a 2XX response', async () => { mockedAxios.delete.mockImplementation(() => Promise.resolve({ status: 200, data: '' })); const args = { - docType: DOCUMENT_TYPE.ARF, + docType: DOCUMENT_TYPE.LLOYD_GEORGE, nhsNumber: '90000000009', baseUrl: '/test', baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, @@ -31,7 +31,7 @@ describe('[DELETE] deleteAllDocuments', () => { test('Delete all documents catches a 4XX response', async () => { mockedAxios.delete.mockImplementation(() => Promise.reject({ status: 403, data: '' })); const args = { - docType: DOCUMENT_TYPE.ARF, + docType: DOCUMENT_TYPE.LLOYD_GEORGE, nhsNumber: '', baseUrl: '/test', baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, @@ -50,7 +50,7 @@ describe('[DELETE] deleteAllDocuments', () => { test('Delete all documents catches a 5XX response', async () => { mockedAxios.delete.mockImplementation(() => Promise.reject({ status: 500, data: '' })); const args = { - docType: DOCUMENT_TYPE.ARF, + docType: DOCUMENT_TYPE.LLOYD_GEORGE, nhsNumber: '', baseUrl: '/test', baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, diff --git a/app/src/helpers/requests/deleteAllDocuments.ts b/app/src/helpers/requests/deleteAllDocuments.ts index 8c587caf6..17259634b 100644 --- a/app/src/helpers/requests/deleteAllDocuments.ts +++ b/app/src/helpers/requests/deleteAllDocuments.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { AuthHeaders } from '../../types/blocks/authHeaders'; -import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types'; +import { DOCUMENT_TYPE } from '../utils/documentType'; type Args = { docType: DOCUMENT_TYPE; diff --git a/app/src/helpers/requests/getDocument.test.ts b/app/src/helpers/requests/getDocument.test.ts new file mode 100644 index 000000000..debd24e0a --- /dev/null +++ b/app/src/helpers/requests/getDocument.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach, Mocked } from 'vitest'; +import axios from 'axios'; +import getDocument, { GetDocumentResponse } from './getDocument'; +import { AuthHeaders } from '../../types/blocks/authHeaders'; +import { endpoints } from '../../types/generic/endpoints'; + +vi.mock('axios'); +const mockedAxios = axios as Mocked; + +describe('getDocument', () => { + const mockArgs = { + nhsNumber: '1234567890', + baseUrl: 'https://api.example.com', + baseHeaders: { 'Content-Type': 'application/json', test: 'test' } as AuthHeaders, + documentId: 'doc-123', + }; + + const mockResponse: GetDocumentResponse = { + url: 'https://example.com/document.pdf', + contentType: 'application/pdf', + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should successfully fetch document and return response', async () => { + mockedAxios.get.mockResolvedValueOnce({ data: mockResponse }); + + const result = await getDocument(mockArgs); + + expect(mockedAxios.get).toHaveBeenCalledWith( + `${mockArgs.baseUrl}${endpoints.DOCUMENT_REFERENCE}/${mockArgs.documentId}`, + { + headers: mockArgs.baseHeaders, + params: { + patientId: mockArgs.nhsNumber, + }, + }, + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw AxiosError when request fails', async () => { + const mockError = new Error('Network Error'); + mockedAxios.get.mockRejectedValueOnce(mockError); + + await expect(getDocument(mockArgs)).rejects.toThrow(mockError); + }); + + it('should construct correct URL with documentId', async () => { + mockedAxios.get.mockResolvedValueOnce({ data: mockResponse }); + + await getDocument(mockArgs); + + expect(mockedAxios.get).toHaveBeenCalledWith( + expect.stringContaining(`/${mockArgs.documentId}`), + expect.any(Object), + ); + }); + + it('should pass correct parameters including patientId', async () => { + mockedAxios.get.mockResolvedValueOnce({ data: mockResponse }); + + await getDocument(mockArgs); + + expect(mockedAxios.get).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + params: { + patientId: mockArgs.nhsNumber, + }, + }), + ); + }); +}); diff --git a/app/src/helpers/requests/getDocument.ts b/app/src/helpers/requests/getDocument.ts new file mode 100644 index 000000000..d40b60041 --- /dev/null +++ b/app/src/helpers/requests/getDocument.ts @@ -0,0 +1,42 @@ +import { AuthHeaders } from '../../types/blocks/authHeaders'; +import { endpoints } from '../../types/generic/endpoints'; +import axios, { AxiosError } from 'axios'; + +type Args = { + nhsNumber: string; + baseUrl: string; + baseHeaders: AuthHeaders; + documentId: string; +}; + +export type GetDocumentResponse = { + url: string; + contentType: string; +}; + +const getDocument = async ({ + nhsNumber, + baseUrl, + baseHeaders, + documentId, +}: Args): Promise => { + const gatewayUrl = baseUrl + endpoints.DOCUMENT_REFERENCE + `/${documentId}`; + + try { + const { data } = await axios.get(gatewayUrl, { + headers: { + ...baseHeaders, + }, + params: { + patientId: nhsNumber, + }, + }); + + return data; + } catch (e) { + const error = e as AxiosError; + throw error; + } +}; + +export default getDocument; diff --git a/app/src/helpers/requests/getDocumentSearchResults.ts b/app/src/helpers/requests/getDocumentSearchResults.ts index 6956fc3b2..9fba2272f 100644 --- a/app/src/helpers/requests/getDocumentSearchResults.ts +++ b/app/src/helpers/requests/getDocumentSearchResults.ts @@ -3,7 +3,7 @@ import { endpoints } from '../../types/generic/endpoints'; import { SearchResult } from '../../types/generic/searchResult'; import axios, { AxiosError } from 'axios'; -import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types'; +import { DOCUMENT_TYPE } from '../utils/documentType'; type Args = { nhsNumber: string; diff --git a/app/src/helpers/requests/getPresignedUrlForZip.test.ts b/app/src/helpers/requests/getPresignedUrlForZip.test.ts index a5481adde..269f5b876 100644 --- a/app/src/helpers/requests/getPresignedUrlForZip.test.ts +++ b/app/src/helpers/requests/getPresignedUrlForZip.test.ts @@ -4,9 +4,9 @@ import getPresignedUrlForZip, { pollForPresignedUrl, requestJobId } from './getP import { endpoints } from '../../types/generic/endpoints'; import { JOB_STATUS, PollingResponse } from '../../types/generic/downloadManifestJobStatus'; import waitForSeconds from '../utils/waitForSeconds'; -import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types'; import { DownloadManifestError } from '../../types/generic/errors'; import { describe, expect, it, vi, Mocked, MockedFunction, afterEach } from 'vitest'; +import { DOCUMENT_TYPE } from '../utils/documentType'; vi.mock('axios'); vi.mock('../utils/waitForSeconds', () => ({ @@ -190,7 +190,7 @@ describe('requestJobId', () => { const postRequestParams = mockedAxios.post.mock.calls[0][2]?.params; expect(postRequestParams).toEqual({ - docType: 'LG', + docType: '16521000000101', docReferences, patientId: nhsNumber, }); diff --git a/app/src/helpers/requests/getPresignedUrlForZip.ts b/app/src/helpers/requests/getPresignedUrlForZip.ts index a93bb7e56..011e684f4 100644 --- a/app/src/helpers/requests/getPresignedUrlForZip.ts +++ b/app/src/helpers/requests/getPresignedUrlForZip.ts @@ -1,11 +1,11 @@ import axios from 'axios'; import { endpoints } from '../../types/generic/endpoints'; import { AuthHeaders } from '../../types/blocks/authHeaders'; -import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types'; import { JOB_STATUS, PollingResponse } from '../../types/generic/downloadManifestJobStatus'; import waitForSeconds from '../utils/waitForSeconds'; import { DownloadManifestError } from '../../types/generic/errors'; import { isRunningInCypress } from '../utils/isLocal'; +import { DOCUMENT_TYPE } from '../utils/documentType'; export const DELAY_BETWEEN_POLLING_IN_SECONDS = isRunningInCypress() ? 0 : 3; diff --git a/app/src/helpers/requests/uploadDocument.test.ts b/app/src/helpers/requests/uploadDocument.test.ts index 692cf2af8..2ba8c9b5c 100644 --- a/app/src/helpers/requests/uploadDocument.test.ts +++ b/app/src/helpers/requests/uploadDocument.test.ts @@ -7,7 +7,6 @@ import { } from '../test/testBuilders'; import { DOCUMENT_STATUS, - DOCUMENT_TYPE, DOCUMENT_UPLOAD_STATE, } from '../../types/pages/UploadDocumentsPage/types'; import uploadDocuments, { @@ -19,6 +18,7 @@ import { describe, expect, it, Mocked, vi, beforeEach } from 'vitest'; import { DocumentStatusResult } from '../../types/generic/uploadResult'; import { endpoints } from '../../types/generic/endpoints'; import { v4 as uuidv4 } from 'uuid'; +import { DOCUMENT_TYPE } from '../utils/documentType'; vi.mock('axios'); @@ -59,7 +59,7 @@ describe('uploadDocuments', () => { expect(mockedAxios.post).toHaveBeenCalledTimes(1); expect(mockedAxios.post).toHaveBeenCalledWith( - baseUrl + endpoints.DOCUMENT_UPLOAD, + baseUrl + endpoints.DOCUMENT_REFERENCE, expect.any(String), { headers: baseHeaders, @@ -100,7 +100,7 @@ describe('uploadDocuments', () => { expect(mockedAxios.put).toHaveBeenCalledTimes(1); expect(mockedAxios.put).toHaveBeenCalledWith( - baseUrl + endpoints.DOCUMENT_UPLOAD + `/${documentReferenceId}`, + baseUrl + endpoints.DOCUMENT_REFERENCE + `/${documentReferenceId}`, expect.any(String), { headers: baseHeaders, @@ -330,7 +330,7 @@ describe('uploadDocuments', () => { }); expect(mockedAxios.post).toHaveBeenCalledWith( - baseUrl + endpoints.DOCUMENT_UPLOAD, + baseUrl + endpoints.DOCUMENT_REFERENCE, expect.any(String), expect.any(Object), ); diff --git a/app/src/helpers/requests/uploadDocuments.ts b/app/src/helpers/requests/uploadDocuments.ts index 7123dc5a8..be3fd71b2 100644 --- a/app/src/helpers/requests/uploadDocuments.ts +++ b/app/src/helpers/requests/uploadDocuments.ts @@ -115,7 +115,7 @@ const uploadDocuments = async ({ const gatewayUrl = baseUrl + - endpoints.DOCUMENT_UPLOAD + + endpoints.DOCUMENT_REFERENCE + (documentReferenceId ? `/${documentReferenceId}` : ''); try { diff --git a/app/src/helpers/test/axeTestHelper.ts b/app/src/helpers/test/axeTestHelper.ts index 72f6dd3ce..a174dcdd4 100644 --- a/app/src/helpers/test/axeTestHelper.ts +++ b/app/src/helpers/test/axeTestHelper.ts @@ -18,6 +18,7 @@ const suppressRules = { 'nested-interactive': { enabled: false }, 'duplicate-id-active': { enabled: false }, 'duplicate-id': { enabled: false }, + 'heading-order': { enabled: false }, }; const SUPPRESS_ACCESSIBILITY_TEST = true; diff --git a/app/src/helpers/test/testBuilders.ts b/app/src/helpers/test/testBuilders.ts index f4eba69d9..ba88d56f1 100644 --- a/app/src/helpers/test/testBuilders.ts +++ b/app/src/helpers/test/testBuilders.ts @@ -1,5 +1,4 @@ import { - DOCUMENT_TYPE, DOCUMENT_UPLOAD_STATE, UploadDocument, DOCUMENT_UPLOAD_STATE as documentUploadStates, @@ -19,6 +18,7 @@ import { DeceasedAccessAuditReasons, PatientAccessAudit, } from '../../types/generic/accessAudit'; +import { DOCUMENT_TYPE } from '../utils/documentType'; const buildUserAuth = (userAuthOverride?: Partial): UserAuth => { const auth: UserAuth = { @@ -78,7 +78,7 @@ const buildDocument = ( state: uploadStatus ?? documentUploadStates.SUCCEEDED, progress: 0, id: uuidv4(), - docType: docType ?? DOCUMENT_TYPE.ARF, + docType: docType ?? DOCUMENT_TYPE.LLOYD_GEORGE, attempts: 0, versionId: '1', }; @@ -114,6 +114,8 @@ const buildSearchResult = (searchResultOverride?: Partial): Search id: '1234qwer-241ewewr', fileSize: 224, version: '1', + documentSnomedCodeType: DOCUMENT_TYPE.LLOYD_GEORGE, + contentType: 'application/pdf', ...searchResultOverride, }; return result; diff --git a/app/src/helpers/test/testDataForPdsNameValidation.ts b/app/src/helpers/test/testDataForPdsNameValidation.ts index 7aa88df1c..97401433f 100644 --- a/app/src/helpers/test/testDataForPdsNameValidation.ts +++ b/app/src/helpers/test/testDataForPdsNameValidation.ts @@ -1,11 +1,11 @@ import { PatientDetails } from '../../types/generic/patientDetails'; import { buildPatientDetails } from './testBuilders'; import { - DOCUMENT_TYPE, DOCUMENT_UPLOAD_STATE as documentUploadStates, UploadDocument, } from '../../types/pages/UploadDocumentsPage/types'; import { v4 as uuidv4 } from 'uuid'; +import { DOCUMENT_TYPE } from '../utils/documentType'; type PdsNameMatchingTestCase = { patientDetails: PatientDetails; diff --git a/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.test.tsx b/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.test.tsx index bfcfd6292..5e859634c 100644 --- a/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.test.tsx +++ b/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.test.tsx @@ -1,40 +1,55 @@ import { render, screen, waitFor } from '@testing-library/react'; -import { act } from 'react'; +import React, { act } from 'react'; import DocumentSearchResultsPage from './DocumentSearchResultsPage'; import userEvent from '@testing-library/user-event'; -import { buildPatientDetails, buildSearchResult } from '../../helpers/test/testBuilders'; -import { routes } from '../../types/generic/routes'; +import { + buildPatientDetails, + buildSearchResult, + buildUserAuth, +} from '../../helpers/test/testBuilders'; +import { routeChildren, routes } from '../../types/generic/routes'; import axios from 'axios'; import usePatient from '../../helpers/hooks/usePatient'; import * as ReactRouter from 'react-router-dom'; import { History, createMemoryHistory } from 'history'; import { runAxeTest } from '../../helpers/test/axeTestHelper'; import { afterEach, beforeEach, describe, expect, it, vi, Mock, Mocked } from 'vitest'; +import SessionProvider, { Session } from '../../providers/sessionProvider/SessionProvider'; +import { REPOSITORY_ROLE } from '../../types/generic/authRole'; +import getDocumentSearchResults from '../../helpers/requests/getDocumentSearchResults'; +import getDocument from '../../helpers/requests/getDocument'; +import useConfig from '../../helpers/hooks/useConfig'; const mockedUseNavigate = vi.fn(); vi.mock('react-router-dom', async () => ({ ...(await vi.importActual('react-router-dom')), - useNavigate: () => mockedUseNavigate, - Link: (props: ReactRouter.LinkProps) => , + useNavigate: (): Mock => mockedUseNavigate, + Link: (props: ReactRouter.LinkProps): React.JSX.Element => , })); vi.mock('axios'); -Date.now = () => new Date('2020-01-01T00:00:00.000Z').getTime(); +Date.now = (): number => new Date('2020-01-01T00:00:00.000Z').getTime(); vi.mock('../../helpers/hooks/useBaseAPIHeaders'); vi.mock('../../helpers/hooks/usePatient'); vi.mock('../../helpers/hooks/useConfig'); +vi.mock('../../helpers/requests/getDocumentSearchResults'); +vi.mock('../../helpers/requests/getDocument'); const mockedAxios = axios as Mocked; const mockedUsePatient = usePatient as Mock; +const mockedGetSearchResults = getDocumentSearchResults as Mock; +const mockedGetDocument = getDocument as Mock; +const mockedUseConfig = useConfig as Mock; const mockPatient = buildPatientDetails(); let history = createMemoryHistory({ - initialEntries: ['/'], + initialEntries: ['/patient/documents'], initialIndex: 0, }); describe('', () => { beforeEach(() => { + sessionStorage.setItem('UserSession', ''); history = createMemoryHistory({ initialEntries: ['/'], initialIndex: 0, @@ -42,41 +57,50 @@ describe('', () => { import.meta.env.VITE_ENVIRONMENT = 'vitest'; mockedUsePatient.mockReturnValue(mockPatient); + mockedUseConfig.mockReturnValue({ + featureFlags: { + uploadDocumentIteration3Enabled: true, + }, + }); }); afterEach(() => { vi.clearAllMocks(); + vi.restoreAllMocks(); }); describe('Rendering', () => { - it('renders the page after a successful response from api', async () => { - mockedAxios.get.mockResolvedValue(async () => { - return Promise.resolve({ data: [buildSearchResult()] }); - }); - - renderPage(history); - - expect( - screen.getByRole('heading', { - name: 'Manage this Lloyd George record', - }), - ).toBeInTheDocument(); - - await waitFor(() => { - expect( - screen.queryByRole('progressbar', { name: 'Loading...' }), - ).not.toBeInTheDocument(); - }); - }); + it.each([ + { role: REPOSITORY_ROLE.GP_ADMIN, title: 'Lloyd George records' }, + { role: REPOSITORY_ROLE.GP_CLINICAL, title: 'Lloyd George records' }, + { role: REPOSITORY_ROLE.PCSE, title: 'Manage Lloyd George records' }, + ])( + 'renders the page after a successful response from api when role is $role', + async ({ role, title }) => { + mockedGetSearchResults.mockResolvedValue([buildSearchResult()]); + + renderPage(history, role); + + const pageTitle = screen.getByTestId('page-title'); + expect(pageTitle).toBeInTheDocument(); + expect(pageTitle).toHaveTextContent(title); + + await waitFor(() => { + expect( + screen.queryByRole('progressbar', { name: 'Loading...' }), + ).not.toBeInTheDocument(); + }); + }, + ); it('displays a progress bar when the document search results are being requested', async () => { - mockedAxios.get.mockImplementation(async () => { + mockedGetSearchResults.mockImplementationOnce(async () => { await new Promise((resolve) => { setTimeout(() => { // To delay the mock request, and give a chance for the progress bar to appear resolve(null); }, 500); }); - return Promise.resolve({ data: [buildSearchResult()] }); + return Promise.resolve([buildSearchResult()]); }); renderPage(history); @@ -85,7 +109,7 @@ describe('', () => { }); it('displays a message when a document search returns no results', async () => { - mockedAxios.get.mockResolvedValue(async () => { + mockedGetSearchResults.mockImplementation(async () => { return Promise.resolve({ data: [] }); }); @@ -103,86 +127,27 @@ describe('', () => { expect(screen.queryByTestId('delete-all-documents-btn')).not.toBeInTheDocument(); }); - it('displays a error messages when the call to document manifest fails', async () => { - mockedAxios.get.mockResolvedValue({ data: [buildSearchResult()] }); - - const errorResponse = { - response: { - status: 403, - message: 'An error occurred', - }, - }; - - renderPage(history); - - mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); - - await waitFor(() => { - screen.getByRole('button', { name: 'Download all documents' }); - }); - await userEvent.click(screen.getByRole('button', { name: 'Download all documents' })); - - expect( - await screen.findByText('An error has occurred while preparing your download'), - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Download all documents' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Remove all documents' }), - ).toBeInTheDocument(); - }); - - it('displays a error messages when the call to document manifest return 400', async () => { - mockedAxios.get.mockResolvedValue({ data: [buildSearchResult()] }); - + it('displays a service error when document search fails with bad request', async () => { const errorResponse = { response: { status: 400, - data: { message: 'An error occurred', err_code: 'SP_1001' }, + message: 'bad request', }, }; + mockedGetSearchResults.mockRejectedValueOnce(errorResponse); renderPage(history); - mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); - await waitFor(() => { - screen.getByRole('button', { name: 'Download all documents' }); + expect(screen.getByTestId('service-error')).toBeInTheDocument(); }); - await userEvent.click(screen.getByRole('button', { name: 'Download all documents' })); - expect( - await screen.findByText('An error has occurred while preparing your download'), - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Download all documents' }), - ).toBeInTheDocument(); - expect(screen.getByTestId('delete-all-documents-btn')).toBeInTheDocument(); - }); - - it('displays a message when a document search return 423 locked error', async () => { - const errorResponse = { - response: { - status: 423, - message: 'An error occurred', - }, - }; - mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); - - renderPage(history); - - expect( - await screen.findByText( - 'There are already files being uploaded for this patient, please try again in a few minutes.', - ), - ).toBeInTheDocument(); }); }); describe('Accessibility', () => { it('pass accessibility checks at loading screen', async () => { - mockedAxios.get.mockReturnValueOnce( - new Promise((resolve) => setTimeout(resolve, 100000)), + mockedGetSearchResults.mockImplementation(() => + new Promise((resolve) => setTimeout(resolve, 20000)), ); renderPage(history); @@ -193,30 +158,33 @@ describe('', () => { }); it('pass accessibility checks when displaying search result', async () => { - mockedAxios.get.mockResolvedValue({ data: [buildSearchResult()] }); + mockedGetSearchResults.mockImplementation(() => Promise.resolve([buildSearchResult()])); renderPage(history); - expect(await screen.findByText('List of documents available')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByTestId('subtitle')).toBeInTheDocument(); + }); const results = await runAxeTest(document.body); expect(results).toHaveNoViolations(); }); it('pass accessibility checks when error boxes are showing up', async () => { - mockedAxios.get.mockResolvedValue({ data: [buildSearchResult()] }); + mockedGetSearchResults.mockImplementation(() => Promise.resolve([buildSearchResult()])); const errorResponse = { response: { status: 400, data: { message: 'An error occurred', err_code: 'SP_1001' }, }, }; - renderPage(history); + renderPage(history, REPOSITORY_ROLE.PCSE); const downloadButton = await screen.findByRole('button', { name: 'Download all documents', }); - mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); + + vi.spyOn(mockedAxios, 'get').mockRejectedValueOnce(errorResponse); await userEvent.click(downloadButton); expect( @@ -232,48 +200,131 @@ describe('', () => { }); describe('Navigation', () => { - it('navigates to Error page when call to doc manifest return 500', async () => { - mockedAxios.get.mockResolvedValue({ data: [buildSearchResult()] }); + it('navigates to session expire page when a document search return 403 unauthorised error', async () => { + const errorResponse = { + response: { + status: 403, + message: 'An error occurred', + }, + }; + mockedGetSearchResults.mockRejectedValueOnce(errorResponse); + + renderPage(history); + + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED); + }); + }); + + it('navigates to server error page when document search return 500 server error', async () => { const errorResponse = { response: { status: 500, - data: { message: 'An error occurred', err_code: 'SP_1001' }, + message: 'An error occurred', }, }; - mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); + mockedGetSearchResults.mockRejectedValueOnce(errorResponse); + + renderPage(history); + + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(expect.stringContaining(routes.SERVER_ERROR)); + }); + }); + + it('loads the document and navigates to view screen on view link clicked', async () => { + mockedGetSearchResults.mockResolvedValue([buildSearchResult()]); + + renderPage(history); + + await waitFor(() => { + expect( + screen.queryByRole('progressbar', { name: 'Loading...' }), + ).not.toBeInTheDocument(); + }); - act(() => { - renderPage(history); + const viewLink = screen.getByTestId('view-0-link'); + await act(async () => { + await userEvent.click(viewLink); }); + expect(mockedGetDocument).toHaveBeenCalledTimes(1); + expect(mockedUseNavigate).toHaveBeenCalledWith(routeChildren.DOCUMENT_VIEW); + }); + + it('navigates to server error when load document fails with 500', async () => { + mockedGetSearchResults.mockResolvedValue([buildSearchResult()]); + + const errorResponse = { + response: { + status: 500, + message: 'server error', + }, + }; + mockedGetDocument.mockRejectedValue(errorResponse); + + renderPage(history); + await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith( - routes.SERVER_ERROR + '?encodedError=WyJTUF8xMDAxIiwiMTU3NzgzNjgwMCJd', - ); + expect( + screen.queryByRole('progressbar', { name: 'Loading...' }), + ).not.toBeInTheDocument(); }); + + const viewLink = screen.getByTestId('view-0-link'); + await act(async () => { + await userEvent.click(viewLink); + }); + + expect(mockedGetDocument).toHaveBeenCalledTimes(1); + expect(mockedUseNavigate).toHaveBeenCalledWith(routeChildren.DOCUMENT_VIEW); + expect(mockedUseNavigate).toHaveBeenCalledWith( + expect.stringContaining(routes.SERVER_ERROR), + ); }); - it('navigates to session expire page when a document search return 403 unauthorised error', async () => { + + it('navigates to session expired when load document fails with 403', async () => { + mockedGetSearchResults.mockResolvedValue([buildSearchResult()]); + const errorResponse = { response: { status: 403, - message: 'An error occurred', + message: 'forbidden', }, }; - mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); + mockedGetDocument.mockRejectedValue(errorResponse); renderPage(history); await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED); + expect( + screen.queryByRole('progressbar', { name: 'Loading...' }), + ).not.toBeInTheDocument(); }); + + const viewLink = screen.getByTestId('view-0-link'); + await act(async () => { + await userEvent.click(viewLink); + }); + + expect(mockedGetDocument).toHaveBeenCalledTimes(1); + expect(mockedUseNavigate).toHaveBeenCalledWith(routeChildren.DOCUMENT_VIEW); + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED); }); }); - const renderPage = (history: History) => { - return render( - - - , + const renderPage = (history: History, role?: REPOSITORY_ROLE): void => { + const auth: Session = { + auth: buildUserAuth({ role: role ?? REPOSITORY_ROLE.GP_ADMIN }), + isLoggedIn: true, + }; + render( + + + + + , + , ); }; }); diff --git a/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx b/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx index 35fb3a02a..79d013311 100644 --- a/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx +++ b/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx @@ -1,27 +1,37 @@ import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import { SearchResult } from '../../types/generic/searchResult'; -import DocumentSearchResults from '../../components/blocks/_arf/documentSearchResults/DocumentSearchResults'; +import DocumentSearchResults from '../../components/blocks/_patientDocuments/documentSearchResults/DocumentSearchResults'; import { Outlet, Route, Routes, useNavigate } from 'react-router-dom'; import { routeChildren, routes } from '../../types/generic/routes'; -import { SUBMISSION_STATE } from '../../types/pages/documentSearchResultsPage/types'; -import ProgressBar from '../../components/generic/progressBar/ProgressBar'; +import { + DocumentReference, + SUBMISSION_STATE, +} from '../../types/pages/documentSearchResultsPage/types'; import ServiceError from '../../components/layout/serviceErrorBox/ServiceErrorBox'; -import DocumentSearchResultsOptions from '../../components/blocks/_arf/documentSearchResultsOptions/DocumentSearchResultsOptions'; -import { AxiosError } from 'axios'; +import DocumentSearchResultsOptions from '../../components/blocks/_patientDocuments/documentSearchResultsOptions/DocumentSearchResultsOptions'; +import axios, { AxiosError } from 'axios'; import getDocumentSearchResults from '../../helpers/requests/getDocumentSearchResults'; import useBaseAPIHeaders from '../../helpers/hooks/useBaseAPIHeaders'; -import DeleteSubmitStage from '../../components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage'; -import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types'; import usePatient from '../../helpers/hooks/usePatient'; import useBaseAPIUrl from '../../helpers/hooks/useBaseAPIUrl'; import ErrorBox from '../../components/layout/errorBox/ErrorBox'; import { errorToParams } from '../../helpers/utils/errorToParams'; import useTitle from '../../helpers/hooks/useTitle'; import { getLastURLPath } from '../../helpers/utils/urlManipulations'; -import PatientSummary from '../../components/generic/patientSummary/PatientSummary'; +import PatientSummary, { + PatientInfo, +} from '../../components/generic/patientSummary/PatientSummary'; import { isMock } from '../../helpers/utils/isLocal'; import useConfig from '../../helpers/hooks/useConfig'; import { buildSearchResult } from '../../helpers/test/testBuilders'; +import { useSessionContext } from '../../providers/sessionProvider/SessionProvider'; +import { REPOSITORY_ROLE } from '../../types/generic/authRole'; +import DocumentView from '../../components/blocks/_patientDocuments/documentView/DocumentView'; +import getDocument, { GetDocumentResponse } from '../../helpers/requests/getDocument'; +import { DOCUMENT_TYPE } from '../../helpers/utils/documentType'; +import BackButton from '../../components/generic/backButton/BackButton'; +import ProgressBar from '../../components/generic/progressBar/ProgressBar'; +import DeleteSubmitStage from '../../components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage'; const DocumentSearchResultsPage = (): React.JSX.Element => { const patientDetails = usePatient(); @@ -30,11 +40,14 @@ const DocumentSearchResultsPage = (): React.JSX.Element => { const [searchResults, setSearchResults] = useState>([]); const [submissionState, setSubmissionState] = useState(SUBMISSION_STATE.INITIAL); const [downloadState, setDownloadState] = useState(SUBMISSION_STATE.INITIAL); + const [documentReference, setDocumentReference] = useState(null); const navigate = useNavigate(); const baseUrl = useBaseAPIUrl(); const baseHeaders = useBaseAPIHeaders(); const config = useConfig(); const mounted = useRef(false); + const activeSearchResult = useRef(null); + const [removeDocType, setRemoveDocType] = useState(undefined); useEffect(() => { const onPageLoad = async (): Promise => { @@ -52,18 +65,31 @@ const DocumentSearchResultsPage = (): React.JSX.Element => { } catch (e) { const error = e as AxiosError; if (isMock(error)) { - if (config.mockLocal.uploading) { - setSubmissionState(SUBMISSION_STATE.BLOCKED); - } else if (config.mockLocal.recordUploaded) { - setSearchResults([buildSearchResult(), buildSearchResult()]); + if (config.mockLocal.recordUploaded) { + setSearchResults([ + buildSearchResult({ + documentSnomedCodeType: DOCUMENT_TYPE.LLOYD_GEORGE, + fileName: 'Scanned paper notes.pdf', + }), + buildSearchResult({ + documentSnomedCodeType: DOCUMENT_TYPE.EHR, + fileName: 'Electronic health record.pdf', + }), + buildSearchResult({ + documentSnomedCodeType: DOCUMENT_TYPE.EHR_ATTACHMENTS, + fileName: 'EHR Attachments.zip', + contentType: 'application/zip', + }), + ]); + setSubmissionState(SUBMISSION_STATE.SUCCEEDED); + } else { + setSearchResults([]); setSubmissionState(SUBMISSION_STATE.SUCCEEDED); } } else if (error.response?.status === 403) { navigate(routes.SESSION_EXPIRED); } else if (error.response?.status && error.response?.status >= 500) { navigate(routes.SERVER_ERROR + errorToParams(error)); - } else if (error.response?.status === 423) { - setSubmissionState(SUBMISSION_STATE.BLOCKED); } else { setSubmissionState(SUBMISSION_STATE.FAILED); } @@ -74,8 +100,63 @@ const DocumentSearchResultsPage = (): React.JSX.Element => { void onPageLoad(); } }, [nhsNumber, navigate, baseUrl, baseHeaders, config]); - const pageHeader = 'Manage this Lloyd George record'; - useTitle({ pageTitle: pageHeader }); + + const onViewDocument = (documentItem: SearchResult): void => { + activeSearchResult.current = documentItem; + setDocumentReference({ + isPdf: documentItem.contentType === 'application/pdf', + ...documentItem, + }); + navigate(routeChildren.DOCUMENT_VIEW); + + void loadDocument(documentItem.id); + }; + + const loadDocument = async (documentId: string): Promise => { + try { + const documentResponse = await getDocument({ + nhsNumber: patientDetails!.nhsNumber, + baseUrl, + baseHeaders, + documentId, + }); + + await handleViewDocSuccess(documentResponse); + } catch (e) { + const error = e as AxiosError; + if (isMock(error)) { + await handleViewDocSuccess({ + url: '/dev/testFile.pdf', + contentType: activeSearchResult.current?.contentType || 'application/pdf', + }); + } else if (error.response?.status === 403) { + navigate(routes.SESSION_EXPIRED); + } else { + navigate(routes.SERVER_ERROR + errorToParams(error)); + } + } + }; + + const handleViewDocSuccess = async (documentResponse: GetDocumentResponse): Promise => { + setDocumentReference({ + url: await getObjectUrl(documentResponse.url), + isPdf: documentResponse.contentType === 'application/pdf', + ...activeSearchResult.current, + } as DocumentReference); + }; + + const getObjectUrl = async (cloudFrontUrl: string): Promise => { + const { data } = await axios.get(cloudFrontUrl, { + responseType: 'blob', + }); + + return URL.createObjectURL(data); + }; + + const removeDocuments = (docType: DOCUMENT_TYPE): void => { + setRemoveDocType(docType); + navigate(routeChildren.DOCUMENT_DELETE); + }; return ( <> @@ -90,18 +171,27 @@ const DocumentSearchResultsPage = (): React.JSX.Element => { downloadState={downloadState} setDownloadState={setDownloadState} searchResults={searchResults} - pageHeader={pageHeader} + onViewDocument={onViewDocument} + /> + } + /> + } /> null} + docType={removeDocType ?? DOCUMENT_TYPE.ALL} + resetDocState={(): void => { + mounted.current = false; + }} /> } /> @@ -118,8 +208,8 @@ type PageIndexArgs = { downloadState: SUBMISSION_STATE; setDownloadState: Dispatch>; searchResults: SearchResult[]; - pageHeader: string; nhsNumber: string; + onViewDocument: (document: SearchResult) => void; }; const DocumentSearchResultsPageIndex = ({ submissionState, @@ -127,49 +217,88 @@ const DocumentSearchResultsPageIndex = ({ searchResults, nhsNumber, setDownloadState, + onViewDocument, }: PageIndexArgs): React.JSX.Element => { - const pageHeader = 'Manage this Lloyd George record'; + const [session] = useSessionContext(); + const patientDetails = usePatient(); + const navigate = useNavigate(); + + const role = session.auth?.role; + + const canViewFiles = + session.auth?.role === REPOSITORY_ROLE.GP_ADMIN || + session.auth?.role === REPOSITORY_ROLE.GP_CLINICAL; + + const pageHeader = canViewFiles ? 'Lloyd George records' : 'Manage Lloyd George records'; useTitle({ pageTitle: pageHeader }); + const SearchResults = (): React.JSX.Element => { + if ( + submissionState === SUBMISSION_STATE.INITIAL || + submissionState === SUBMISSION_STATE.PENDING + ) { + return ; + } + + if (searchResults.length && nhsNumber) { + return ( + <> + + + {role === REPOSITORY_ROLE.PCSE && ( + + )} + + ); + } + + return ( +

+ + There are no documents available for this patient. + +

+ ); + }; + + if (!session.auth) { + navigate(routes.UNAUTHORISED); + return <>; + } + return ( <> -

{pageHeader}

+ + +

+ {pageHeader} +

{(submissionState === SUBMISSION_STATE.FAILED || downloadState === SUBMISSION_STATE.FAILED) && } - - - {submissionState === SUBMISSION_STATE.PENDING && ( - - )} - {submissionState === SUBMISSION_STATE.BLOCKED && ( -

- There are already files being uploaded for this patient, please try again in a - few minutes. -

- )} + + + + + - {submissionState === SUBMISSION_STATE.SUCCEEDED && ( - <> - {searchResults.length && nhsNumber ? ( - <> - - - - ) : ( -

- - There are no documents available for this patient. - -

- )} - - )} + {downloadState === SUBMISSION_STATE.FAILED && ( { journey: 'update', existingDocuments: [ { - docType: OLD_DOCUMENT_TYPE.LLOYD_GEORGE, + docType: DOCUMENT_TYPE.LLOYD_GEORGE, blob: mockBlob, fileName: 'test.pdf', documentId: 'doc-123', @@ -280,7 +279,7 @@ describe('DocumentUploadPage', (): void => { const testDocument = buildDocument( testFile, DOCUMENT_UPLOAD_STATE.SELECTED, - OLD_DOCUMENT_TYPE.LLOYD_GEORGE, + DOCUMENT_TYPE.LLOYD_GEORGE, ); const mockUploadSession = buildUploadSession([testDocument]); @@ -297,7 +296,7 @@ describe('DocumentUploadPage', (): void => { const testDocument = buildDocument( testFile, DOCUMENT_UPLOAD_STATE.UPLOADING, - OLD_DOCUMENT_TYPE.LLOYD_GEORGE, + DOCUMENT_TYPE.LLOYD_GEORGE, ); testDocument.ref = 'test-ref-123'; diff --git a/app/src/pages/documentUploadPage/DocumentUploadPage.tsx b/app/src/pages/documentUploadPage/DocumentUploadPage.tsx index 89828a768..27f2cd09f 100644 --- a/app/src/pages/documentUploadPage/DocumentUploadPage.tsx +++ b/app/src/pages/documentUploadPage/DocumentUploadPage.tsx @@ -32,14 +32,9 @@ import { useEnhancedNavigate, } from '../../helpers/utils/urlManipulations'; import { routeChildren, routes } from '../../types/generic/routes'; -import { - DocumentStatusResult, - S3UploadFields, - UploadSession, -} from '../../types/generic/uploadResult'; +import { DocumentStatusResult, UploadSession } from '../../types/generic/uploadResult'; import { DOCUMENT_STATUS, - DOCUMENT_TYPE, DOCUMENT_UPLOAD_STATE, UploadDocument, } from '../../types/pages/UploadDocumentsPage/types'; @@ -47,6 +42,7 @@ import documentTypesConfig from '../../config/documentTypesConfig.json'; import { Card } from 'nhsuk-react-components'; import { ReactComponent as RightCircleIcon } from '../../styles/right-chevron-circle.svg'; import PatientSummary from '../../components/generic/patientSummary/PatientSummary'; +import { DOCUMENT_TYPE } from '../../helpers/utils/documentType'; type LocationState = { journey?: JourneyType; @@ -208,11 +204,8 @@ const DocumentUploadPage = (): React.JSX.Element => { const session: UploadSession = {}; documents.forEach((doc) => { session[doc.id] = { - url: 'https://example.com', - fields: { - key: `https://example.com/${uuidv4()}`, - } as S3UploadFields, - }; + url: 'https://dusafgdswgfew4-staging-bulk-store.s3.eu-west-2.amazonaws.com/user_upload/9730153817/91b73c0f-b5b0-49f1-acbe-b0a5752dc3df?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAXYSUA44V5SE2IC6U%2F20251028%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251028T162320Z&X-Amz-Expires=1800&X-Amz-SignedHeaders=host&X-Amz-Security-Token=FwoGZXIvYXdzEBoaDCqX56UT2MdBQk7ztCLIAWXO7781OXoLLc3gJN9UQcAZlaoEhwJl5FQfKuJvn32DAPwYhbS80rb0JGIYmF8rIqj7TKbNOfaw4t%2Bq5NUO%2FEDQLxRbSpl8%2B078%2Ba9d2pY5XbPH3u6D0nW9mzNVREwg1%2Bt02HnWp9YLdREyDO4is9Fj5P3SQRh6DydzLx3in%2BZzzwVK8prxGG%2BBYRn5cQVOKcQCtAR7NMhHhTz9GeFQxU6X5YNalZdZdRJoFmdkxkpdoFeoIozs2Kg6plZhnqbWpFIrV3GvmYTDKPfbg8gGMi2c6f%2F9IJpIscXn0RfQZYA8lr02VHjBtez0LgzKcGVXYsE666uclkspOgBxpgo%3D&X-Amz-Signature=fdf6e3d7522ab4fe80156510d1318c430d4a44170fb98924cdc231117b5eafb8', + } as any; }); return session; diff --git a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx index e0279262b..68d8fb91e 100644 --- a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx +++ b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx @@ -21,6 +21,7 @@ import getLloydGeorgeRecord from '../../helpers/requests/getLloydGeorgeRecord'; import useBaseAPIHeaders from '../../helpers/hooks/useBaseAPIHeaders'; import useBaseAPIUrl from '../../helpers/hooks/useBaseAPIUrl'; import { getFormattedDatetime } from '../../helpers/utils/formatDatetime'; +import { DOCUMENT_TYPE } from '../../helpers/utils/documentType'; const LloydGeorgeRecordPage = (): React.JSX.Element => { const [downloadStage, setDownloadStage] = useState(DOWNLOAD_STAGE.INITIAL); @@ -138,8 +139,7 @@ const LloydGeorgeRecordPage = (): React.JSX.Element => { element={ } diff --git a/app/src/pages/patientResultPage/PatientResultPage.test.tsx b/app/src/pages/patientResultPage/PatientResultPage.test.tsx index 1d2b5417d..aa80bd310 100644 --- a/app/src/pages/patientResultPage/PatientResultPage.test.tsx +++ b/app/src/pages/patientResultPage/PatientResultPage.test.tsx @@ -8,6 +8,7 @@ import useRole from '../../helpers/hooks/useRole'; import usePatient from '../../helpers/hooks/usePatient'; import { runAxeTest } from '../../helpers/test/axeTestHelper'; import { afterEach, beforeEach, describe, expect, it, vi, Mock } from 'vitest'; +import useConfig from '../../helpers/hooks/useConfig'; const mockedUseNavigate = vi.fn(); vi.mock('react-router-dom', () => ({ @@ -16,9 +17,11 @@ vi.mock('react-router-dom', () => ({ })); vi.mock('../../helpers/hooks/useRole'); vi.mock('../../helpers/hooks/usePatient'); +vi.mock('../../helpers/hooks/useConfig'); const mockedUseRole = useRole as Mock; const mockedUsePatient = usePatient as Mock; +const mockedUseConfig = useConfig as Mock; const PAGE_HEADER_TEXT = 'Patient details'; const PAGE_TEXT = @@ -29,6 +32,15 @@ describe('PatientResultPage', () => { beforeEach(() => { import.meta.env.VITE_ENVIRONMENT = 'vitest'; mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); + vi.mocked(useConfig).mockReturnValue({ + featureFlags: { + uploadLambdaEnabled: true, + uploadArfWorkflowEnabled: false, + uploadLloydGeorgeWorkflowEnabled: true, + uploadDocumentIteration3Enabled: false, + }, + mockLocal: {}, + }); }); afterEach(() => { vi.clearAllMocks(); @@ -247,7 +259,7 @@ describe('PatientResultPage', () => { }); it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( - "navigates to Lloyd George Record page after user selects Active patient, when role is '%s'", + "navigates to Lloyd George Record page after user selects Active patient, when role is '%s' and uploadDocumentIteration3Enabled is false", async (role) => { const patient = buildPatientDetails({ active: true }); mockedUseRole.mockReturnValue(role); @@ -263,6 +275,32 @@ describe('PatientResultPage', () => { }, ); + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "navigates to patient documents page after user selects Active patient, when role is '%s' and uploadDocumentIteration3Enabled is true", + async (role) => { + const patient = buildPatientDetails({ active: true }); + mockedUseRole.mockReturnValue(role); + mockedUsePatient.mockReturnValue(patient); + mockedUseConfig.mockReturnValue({ + featureFlags: { + uploadLambdaEnabled: true, + uploadArfWorkflowEnabled: false, + uploadLloydGeorgeWorkflowEnabled: true, + uploadDocumentIteration3Enabled: true, + }, + mockLocal: {}, + }); + + render(); + + await userEvent.click(screen.getByRole('button', { name: CONFIRM_BUTTON_TEXT })); + + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.PATIENT_DOCUMENTS); + }); + }, + ); + it('navigates to ARF Download page when user selects Verify patient, when role is PCSE', async () => { const role = REPOSITORY_ROLE.PCSE; mockedUseRole.mockReturnValue(role); @@ -272,7 +310,7 @@ describe('PatientResultPage', () => { await userEvent.click(screen.getByRole('button', { name: CONFIRM_BUTTON_TEXT })); await waitFor(() => { - expect(mockedUseNavigate).toHaveBeenCalledWith(routes.ARF_OVERVIEW); + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.PATIENT_DOCUMENTS); }); }); diff --git a/app/src/pages/patientResultPage/PatientResultPage.tsx b/app/src/pages/patientResultPage/PatientResultPage.tsx index 6aaf8ce63..0709b302e 100644 --- a/app/src/pages/patientResultPage/PatientResultPage.tsx +++ b/app/src/pages/patientResultPage/PatientResultPage.tsx @@ -10,6 +10,7 @@ import useRole from '../../helpers/hooks/useRole'; import usePatient from '../../helpers/hooks/usePatient'; import useTitle from '../../helpers/hooks/useTitle'; import PatientSummary from '../../components/generic/patientSummary/PatientSummary'; +import useConfig from '../../helpers/hooks/useConfig'; const PatientResultPage = (): React.JSX.Element => { const role = useRole(); @@ -18,11 +19,12 @@ const PatientResultPage = (): React.JSX.Element => { const navigate = useNavigate(); const [inputError, setInputError] = useState(''); const { handleSubmit } = useForm(); + const { featureFlags } = useConfig(); const submit = (): void => { if (userIsPCSE) { // Make PDS and Dynamo document store search request to download documents from patient - navigate(routes.ARF_OVERVIEW); + navigate(routes.PATIENT_DOCUMENTS); } else { // Make PDS patient search request to upload documents to patient if (typeof patientDetails?.active === 'undefined') { @@ -36,13 +38,18 @@ const PatientResultPage = (): React.JSX.Element => { } if (patientDetails?.active) { - navigate(routes.LLOYD_GEORGE); + navigate( + featureFlags.uploadDocumentIteration3Enabled + ? routes.PATIENT_DOCUMENTS + : routes.LLOYD_GEORGE, + ); return; } navigate(routes.SEARCH_PATIENT); } }; + const showDeceasedWarning = patientDetails?.deceased && !userIsPCSE; const showWarning = patientDetails?.superseded || patientDetails?.restricted || showDeceasedWarning; diff --git a/app/src/router/AppRouter.tsx b/app/src/router/AppRouter.tsx index 4b72c0c51..4e0e73adc 100644 --- a/app/src/router/AppRouter.tsx +++ b/app/src/router/AppRouter.tsx @@ -9,7 +9,7 @@ import UnauthorisedPage from '../pages/unauthorisedPage/UnauthorisedPage'; import LogoutPage from '../pages/logoutPage/LogoutPage'; import PatientSearchPage from '../pages/patientSearchPage/PatientSearchPage'; import PatientResultPage from '../pages/patientResultPage/PatientResultPage'; -import ArfSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage'; +import PatientDocumentSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage'; import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import AuthGuard from './guards/authGuard/AuthGuard'; import PatientGuard from './guards/patientGuard/PatientGuard'; @@ -46,8 +46,8 @@ const { PRIVACY_POLICY, LLOYD_GEORGE, LLOYD_GEORGE_WILDCARD, - ARF_OVERVIEW, - ARF_OVERVIEW_WILDCARD, + PATIENT_DOCUMENTS, + PATIENT_DOCUMENTS_WILDCARD, REPORT_DOWNLOAD, REPORT_DOWNLOAD_WILDCARD, PATIENT_ACCESS_AUDIT, @@ -91,16 +91,20 @@ export const childRoutes = [ parent: LLOYD_GEORGE, }, { - route: routeChildren.ARF_DELETE, - parent: ARF_OVERVIEW, + route: routeChildren.DOCUMENT_VIEW, + parent: PATIENT_DOCUMENTS, }, { - route: routeChildren.ARF_DELETE_CONFIRMATION, - parent: ARF_OVERVIEW, + route: routeChildren.DOCUMENT_DELETE, + parent: PATIENT_DOCUMENTS, }, { - route: routeChildren.ARF_DELETE_COMPLETE, - parent: ARF_OVERVIEW, + route: routeChildren.DOCUMENT_DELETE_CONFIRMATION, + parent: PATIENT_DOCUMENTS, + }, + { + route: routeChildren.DOCUMENT_DELETE_COMPLETE, + parent: PATIENT_DOCUMENTS, }, { route: routeChildren.REPORT_DOWNLOAD_COMPLETE, @@ -239,15 +243,13 @@ export const routeMap: Routes = { type: ROUTE_TYPE.PATIENT, unauthorized: [REPOSITORY_ROLE.PCSE], }, - [ARF_OVERVIEW]: { - page: , + [PATIENT_DOCUMENTS]: { + page: , type: ROUTE_TYPE.PATIENT, - unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, - [ARF_OVERVIEW_WILDCARD]: { - page: , + [PATIENT_DOCUMENTS_WILDCARD]: { + page: , type: ROUTE_TYPE.PATIENT, - unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, [PATIENT_ACCESS_AUDIT]: { page: , diff --git a/app/src/styles/App.scss b/app/src/styles/App.scss index 2743eb059..83f7d9873 100644 --- a/app/src/styles/App.scss +++ b/app/src/styles/App.scss @@ -301,9 +301,6 @@ $hunit: '%'; margin-bottom: 0; &-content { - @extend .nhsuk-body-s; - position: relative; - &-label { font-weight: 700; font-size: 1.5rem; @@ -1311,3 +1308,5 @@ progress:not(.continuous-progress-bar) { } } } + +@import '../components/blocks/_patientDocuments/documentSearchResults/DocumentSearchResults.scss' \ No newline at end of file diff --git a/app/src/types/blocks/lloydGeorgeActions.test.ts b/app/src/types/blocks/lloydGeorgeActions.test.ts index 654f332bd..29c070d5b 100644 --- a/app/src/types/blocks/lloydGeorgeActions.test.ts +++ b/app/src/types/blocks/lloydGeorgeActions.test.ts @@ -10,12 +10,12 @@ describe('getUserRecordActionLinks', () => { const expectedOutput = expect.arrayContaining([ expect.objectContaining({ label: 'Remove record', - key: 'delete-all-files-link', + key: 'delete-files-link', type: RECORD_ACTION.UPDATE, }), expect.objectContaining({ label: 'Download record', - key: 'download-all-files-link', + key: 'download-files-link', type: RECORD_ACTION.DOWNLOAD, }), ]); diff --git a/app/src/types/blocks/lloydGeorgeActions.ts b/app/src/types/blocks/lloydGeorgeActions.ts index 342c64142..94780e773 100644 --- a/app/src/types/blocks/lloydGeorgeActions.ts +++ b/app/src/types/blocks/lloydGeorgeActions.ts @@ -5,6 +5,7 @@ import { LG_RECORD_STAGE } from './lloydGeorgeStages'; export enum RECORD_ACTION { UPDATE = 0, DOWNLOAD = 1, + DELETE = 2, } type ActionRoute = routeChildren | routes; @@ -23,7 +24,7 @@ export type LGRecordActionLink = { export const lloydGeorgeRecordLinks: Array = [ { label: 'Remove record', - key: 'delete-all-files-link', + key: 'delete-files-link', type: RECORD_ACTION.UPDATE, unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], href: routeChildren.LLOYD_GEORGE_DELETE, @@ -31,7 +32,7 @@ export const lloydGeorgeRecordLinks: Array = [ }, { label: 'Download record', - key: 'download-all-files-link', + key: 'download-files-link', type: RECORD_ACTION.DOWNLOAD, unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], href: routeChildren.LLOYD_GEORGE_DOWNLOAD, diff --git a/app/src/types/generic/endpoints.ts b/app/src/types/generic/endpoints.ts index 38d653731..37c311398 100644 --- a/app/src/types/generic/endpoints.ts +++ b/app/src/types/generic/endpoints.ts @@ -6,7 +6,7 @@ export enum endpoints { PATIENT_SEARCH = '/SearchPatient', DOCUMENT_SEARCH = '/SearchDocumentReferences', - DOCUMENT_UPLOAD = '/DocumentReference', + DOCUMENT_REFERENCE = '/DocumentReference', DOCUMENT_PRESIGN = '/DocumentManifest', LLOYDGEORGE_STITCH = '/LloydGeorgeStitch', diff --git a/app/src/types/generic/routes.ts b/app/src/types/generic/routes.ts index 668dae40d..157916363 100644 --- a/app/src/types/generic/routes.ts +++ b/app/src/types/generic/routes.ts @@ -17,8 +17,8 @@ export enum routes { VERIFY_PATIENT = '/patient/verify', LLOYD_GEORGE = '/patient/lloyd-george-record', LLOYD_GEORGE_WILDCARD = '/patient/lloyd-george-record/*', - ARF_OVERVIEW = '/patient/arf', - ARF_OVERVIEW_WILDCARD = '/patient/arf/*', + PATIENT_DOCUMENTS = '/patient/documents', + PATIENT_DOCUMENTS_WILDCARD = '/patient/documents/*', FEEDBACK_CONFIRMATION = '/feedback/confirmation', REPORT_DOWNLOAD = '/create-report', REPORT_DOWNLOAD_WILDCARD = '/create-report/*', @@ -27,6 +27,7 @@ export enum routes { DOCUMENT_UPLOAD = '/patient/document-upload', DOCUMENT_UPLOAD_WILDCARD = '/patient/document-upload/*', + MOCK_LOGIN = 'Auth/MockLogin', } @@ -38,9 +39,6 @@ export enum routeChildren { LLOYD_GEORGE_DELETE = '/patient/lloyd-george-record/delete', LLOYD_GEORGE_DELETE_CONFIRMATION = '/patient/lloyd-george-record/delete/confirmation', LLOYD_GEORGE_DELETE_COMPLETE = '/patient/lloyd-george-record/delete/complete', - ARF_DELETE = '/patient/arf/delete', - ARF_DELETE_CONFIRMATION = '/patient/arf/delete/confirmation', - ARF_DELETE_COMPLETE = '/patient/arf/delete/complete', REPORT_DOWNLOAD_COMPLETE = '/create-report/complete', PATIENT_ACCESS_AUDIT_DECEASED = '/patient/access-audit/deceased', @@ -52,6 +50,11 @@ export enum routeChildren { DOCUMENT_UPLOAD_COMPLETED = '/patient/document-upload/completed', DOCUMENT_UPLOAD_INFECTED = '/patient/document-upload/infected', DOCUMENT_UPLOAD_FILE_ERRORS = '/patient/document-upload/file-errors', + + DOCUMENT_VIEW = '/patient/documents/view', + DOCUMENT_DELETE = '/patient/documents/delete', + DOCUMENT_DELETE_CONFIRMATION = '/patient/documents/delete/confirmation', + DOCUMENT_DELETE_COMPLETE = '/patient/documents/delete/complete', } export enum ROUTE_TYPE { diff --git a/app/src/types/generic/searchResult.ts b/app/src/types/generic/searchResult.ts index d9a6d593c..20c6a7dc6 100644 --- a/app/src/types/generic/searchResult.ts +++ b/app/src/types/generic/searchResult.ts @@ -1,3 +1,5 @@ +import { DOCUMENT_TYPE } from '../../helpers/utils/documentType'; + export type SearchResult = { fileName: string; created: string; @@ -5,4 +7,6 @@ export type SearchResult = { id: string; fileSize: number; version: string; + documentSnomedCodeType: DOCUMENT_TYPE; + contentType: string; }; diff --git a/app/src/types/pages/UploadDocumentsPage/types.ts b/app/src/types/pages/UploadDocumentsPage/types.ts index 1ee793639..58b232f2b 100644 --- a/app/src/types/pages/UploadDocumentsPage/types.ts +++ b/app/src/types/pages/UploadDocumentsPage/types.ts @@ -1,5 +1,6 @@ import type { Dispatch, FormEvent, SetStateAction } from 'react'; import { UPLOAD_FILE_ERROR_TYPE } from '../../../helpers/utils/fileUploadErrorMessages'; +import { DOCUMENT_TYPE } from '../../../helpers/utils/documentType'; export type SetUploadStage = Dispatch>; export type SetUploadDocuments = Dispatch>>; @@ -9,12 +10,6 @@ export enum UPLOAD_STAGE { Complete = 2, } -export enum DOCUMENT_TYPE { - LLOYD_GEORGE = 'LG', - ARF = 'ARF', - ALL = 'LG,ARF', -} - export enum DOCUMENT_UPLOAD_STATE { SELECTED = 'SELECTED', UPLOADING = 'UPLOADING', diff --git a/app/src/types/pages/documentSearchResultsPage/types.ts b/app/src/types/pages/documentSearchResultsPage/types.ts index 298be815a..74407a0d3 100644 --- a/app/src/types/pages/documentSearchResultsPage/types.ts +++ b/app/src/types/pages/documentSearchResultsPage/types.ts @@ -1,3 +1,5 @@ +import { SearchResult } from '../../generic/searchResult'; + export enum SUBMISSION_STATE { INITIAL = 'INITIAL', PENDING = 'PENDING', @@ -12,3 +14,8 @@ export enum SEARCH_AND_DOWNLOAD_STATE { SEARCH_SUCCEEDED = 'SEARCH_SUCCEEDED', DOWNLOAD_SELECTED = 'DOWNLOAD_SELECTED', } + +export type DocumentReference = SearchResult & { + url?: string | null; + isPdf?: boolean; +}; diff --git a/lambdas/enums/supported_document_types.py b/lambdas/enums/supported_document_types.py index 5eb393620..38412c770 100644 --- a/lambdas/enums/supported_document_types.py +++ b/lambdas/enums/supported_document_types.py @@ -1,6 +1,7 @@ import os from enum import StrEnum +from enums.snomed_codes import SnomedCodes from utils.audit_logging_setup import LoggingService logger = LoggingService(__name__) @@ -8,7 +9,7 @@ class SupportedDocumentTypes(StrEnum): ARF = "ARF" - LG = "LG" + LG = SnomedCodes.LLOYD_GEORGE.value.code @staticmethod def list(): @@ -19,11 +20,11 @@ def get_dynamodb_table_name(self) -> str: Get the dynamodb table name related to a specific doc_type example usage: - SupportedDocumentTypes.ARF.get_dynamodb_table_name() - (returns "ndr*_DocumentReferenceMetadata") + SupportedDocumentTypes.LG.get_dynamodb_table_name() + (returns "ndr*_LloydGeorgeDocumentReferenceMetadata") result: - "ndr*_DocumentReferenceMetadata" + "ndr*_LloydGeorgeDocumentReferenceMetadata" Eventually we could replace all os.environ["XXX_DYNAMODB_NAME"] calls with this method, so that the logic of resolving table names are controlled in one place. diff --git a/lambdas/services/document_reference_search_service.py b/lambdas/services/document_reference_search_service.py index e3ea9ab75..e039ad834 100644 --- a/lambdas/services/document_reference_search_service.py +++ b/lambdas/services/document_reference_search_service.py @@ -9,6 +9,7 @@ from enums.metadata_field_names import DocumentReferenceMetadataFields from enums.mtls import MtlsCommonNames from enums.snomed_codes import SnomedCodes +from enums.supported_document_types import SupportedDocumentTypes from models.document_reference import DocumentReference from models.fhir.R4.bundle import Bundle, BundleEntry from models.fhir.R4.fhir_document_reference import Attachment, DocumentReferenceInfo @@ -183,6 +184,8 @@ def _build_document_model(self, document: DocumentReference) -> dict: "virus_scanner_result", "file_size", "version", + "content_type", + "document_snomed_code_type", }, ) return document_formatted diff --git a/lambdas/tests/unit/handlers/conftest.py b/lambdas/tests/unit/handlers/conftest.py index f5f29ab3d..32f659f10 100755 --- a/lambdas/tests/unit/handlers/conftest.py +++ b/lambdas/tests/unit/handlers/conftest.py @@ -37,7 +37,7 @@ def valid_id_post_event_with_auth_header(): def valid_id_and_both_doctype_event(): api_gateway_proxy_event = { "httpMethod": "GET", - "queryStringParameters": {"patientId": "9000000009", "docType": "LG,ARF"}, + "queryStringParameters": {"patientId": "9000000009", "docType": "16521000000101,ARF"}, } return api_gateway_proxy_event @@ -55,7 +55,7 @@ def valid_id_and_arf_doctype_event(): def valid_id_and_lg_doctype_event(): api_gateway_proxy_event = { "httpMethod": "GET", - "queryStringParameters": {"patientId": "9000000009", "docType": "LG"}, + "queryStringParameters": {"patientId": "9000000009", "docType": "16521000000101"}, } return api_gateway_proxy_event @@ -64,7 +64,7 @@ def valid_id_and_lg_doctype_event(): def valid_id_and_lg_doctype_delete_event(): api_gateway_proxy_event = { "httpMethod": "DELETE", - "queryStringParameters": {"patientId": "9000000009", "docType": "LG"}, + "queryStringParameters": {"patientId": "9000000009", "docType": "16521000000101"}, } return api_gateway_proxy_event @@ -131,6 +131,24 @@ def mock_upload_document_iteration2_disabled(mocker): ] yield mock_function +@pytest.fixture +def mock_upload_document_iteration3_enabled(mocker): + mock_function = mocker.patch.object(FeatureFlagService, "get_feature_flags_by_flag") + mock_function.side_effect = [ + {"uploadLambdaEnabled": True}, + {"uploadDocumentIteration3Enabled": True}, + ] + yield mock_function + +@pytest.fixture +def mock_upload_document_iteration3_disabled(mocker): + mock_function = mocker.patch.object(FeatureFlagService, "get_feature_flags_by_flag") + mock_function.side_effect = [ + {"uploadLambdaEnabled": True}, + {"uploadDocumentIteration3Enabled": False}, + ] + yield mock_function + @pytest.fixture def mock_validation_strict_disabled(mocker): diff --git a/lambdas/tests/unit/handlers/test_delete_document_reference_handler.py b/lambdas/tests/unit/handlers/test_delete_document_reference_handler.py index 0933842da..520060a61 100644 --- a/lambdas/tests/unit/handlers/test_delete_document_reference_handler.py +++ b/lambdas/tests/unit/handlers/test_delete_document_reference_handler.py @@ -26,19 +26,19 @@ def mock_handle_delete(mocker): [ { "httpMethod": "GET", - "queryStringParameters": {"patientId": "9000000009", "docType": "LG,ARF"}, + "queryStringParameters": {"patientId": "9000000009", "docType": "16521000000101,ARF"}, }, { "httpMethod": "GET", - "queryStringParameters": {"patientId": "9000000009", "docType": "ARF,LG"}, + "queryStringParameters": {"patientId": "9000000009", "docType": "ARF,16521000000101"}, }, { "httpMethod": "GET", - "queryStringParameters": {"patientId": "9000000009", "docType": "LG , ARF"}, + "queryStringParameters": {"patientId": "9000000009", "docType": "16521000000101 , ARF"}, }, { "httpMethod": "GET", - "queryStringParameters": {"patientId": "9000000009", "docType": "ARF, LG"}, + "queryStringParameters": {"patientId": "9000000009", "docType": "ARF, 16521000000101"}, }, ], ) diff --git a/lambdas/tests/unit/handlers/test_document_manifest_job_handler.py b/lambdas/tests/unit/handlers/test_document_manifest_job_handler.py index 6691f99f2..7c97d3d3b 100644 --- a/lambdas/tests/unit/handlers/test_document_manifest_job_handler.py +++ b/lambdas/tests/unit/handlers/test_document_manifest_job_handler.py @@ -21,7 +21,7 @@ def valid_id_and_both_doctype_post_event(): api_gateway_proxy_event = { "httpMethod": "POST", - "queryStringParameters": {"patientId": TEST_NHS_NUMBER, "docType": "LG,ARF"}, + "queryStringParameters": {"patientId": TEST_NHS_NUMBER, "docType": "16521000000101,ARF"}, "multiValueQueryStringParameters": {}, } return api_gateway_proxy_event @@ -41,7 +41,7 @@ def valid_id_and_arf_doctype_post_event(): def valid_id_and_lg_doctype_post_event(): api_gateway_proxy_event = { "httpMethod": "POST", - "queryStringParameters": {"patientId": TEST_NHS_NUMBER, "docType": "LG"}, + "queryStringParameters": {"patientId": TEST_NHS_NUMBER, "docType": "16521000000101"}, "multiValueQueryStringParameters": {}, } return api_gateway_proxy_event @@ -63,7 +63,7 @@ def valid_id_and_lg_doctype_post_event_with_doc_references(): "httpMethod": "POST", "queryStringParameters": { "patientId": TEST_NHS_NUMBER, - "docType": "LG", + "docType": "16521000000101", }, "multiValueQueryStringParameters": { "docReferences": ["test-doc-ref", "test-doc-ref2"], diff --git a/lambdas/tests/unit/helpers/data/create_document_reference.py b/lambdas/tests/unit/helpers/data/create_document_reference.py index 7abcfe915..cfdd20548 100644 --- a/lambdas/tests/unit/helpers/data/create_document_reference.py +++ b/lambdas/tests/unit/helpers/data/create_document_reference.py @@ -29,19 +29,19 @@ { "fileName": f"1of3_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid4", }, { "fileName": f"2of3_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid5", }, { "fileName": f"3of3_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid6", }, ] @@ -53,21 +53,21 @@ { "fileName": f"1of3_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid1", "versionId": "1" }, { "fileName": f"2of3_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid2", "versionId": "2" }, { "fileName": f"3of3_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid3", "versionId": "3" }, @@ -76,7 +76,7 @@ LG_FILE = { "fileName": f"1of1_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid1", "versionId": "1" } @@ -85,7 +85,7 @@ UploadRequestDocument( file_name=f"{i}of3_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", content_type="application/pdf", - doc_type="LG", + doc_type="16521000000101", client_id=f"uuid{i}", version_id=f"{i}" ) @@ -115,7 +115,7 @@ { "fileName": f"1of1_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "text/plain", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid1", } ] @@ -134,7 +134,7 @@ { "fileName": f"1of1_BAD_NAME_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid1", } ] @@ -153,7 +153,7 @@ { "fileName": f"1of3_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid1", } ] @@ -172,13 +172,13 @@ { "fileName": f"1of2_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid1", }, { "fileName": f"1of2_Lloyd_George_Record_[Joe Bloggs]_[{TEST_NHS_NUMBER}]_[25-12-2019].pdf", "contentType": "application/pdf", - "docType": "LG", + "docType": "16521000000101", "clientId": "uuid2", }, ] diff --git a/lambdas/tests/unit/helpers/data/dynamo/dynamo_responses.py b/lambdas/tests/unit/helpers/data/dynamo/dynamo_responses.py index 423669fee..b957c819f 100755 --- a/lambdas/tests/unit/helpers/data/dynamo/dynamo_responses.py +++ b/lambdas/tests/unit/helpers/data/dynamo/dynamo_responses.py @@ -13,6 +13,7 @@ "Uploaded": "True", "Uploading": "False", "LastUpdated": 1704110400, # Timestamp: 2024-01-01T12:00:00 + "DocumentSnomedCodeType": "16521000000101", }, { "ID": "4d8683b9-1665-40d2-8499-6e8302d507ff", @@ -27,6 +28,7 @@ "Uploaded": "True", "Uploading": "False", "LastUpdated": 1704110400, # Timestamp: 2024-01-01T12:00:00 + "DocumentSnomedCodeType": "16521000000101", }, { "ID": "5d8683b9-1665-40d2-8499-6e8302d507ff", @@ -41,6 +43,7 @@ "Uploaded": "True", "Uploading": "False", "LastUpdated": 1704110400, # Timestamp: 2024-01-01T12:00:00 + "DocumentSnomedCodeType": "16521000000101", }, ], "Count": 3, diff --git a/lambdas/tests/unit/helpers/data/upload_confirm_result.py b/lambdas/tests/unit/helpers/data/upload_confirm_result.py index 48e993f5f..658a35dbe 100644 --- a/lambdas/tests/unit/helpers/data/upload_confirm_result.py +++ b/lambdas/tests/unit/helpers/data/upload_confirm_result.py @@ -4,11 +4,11 @@ MOCK_ARF_DOCUMENTS = {"ARF": [TEST_FILE_KEY, TEST_FILE_KEY]} MOCK_ARF_DOCUMENT_REFERENCES = [TEST_FILE_KEY, TEST_FILE_KEY] -MOCK_LG_SINGLE_DOCUMENT = {"LG": [TEST_FILE_KEY]} +MOCK_LG_SINGLE_DOCUMENT = {"16521000000101": [TEST_FILE_KEY]} MOCK_LG_SINGLE_DOCUMENT_REFERENCES = [TEST_FILE_KEY] -MOCK_LG_DOCUMENTS = {"LG": [TEST_FILE_KEY, TEST_FILE_KEY]} +MOCK_LG_DOCUMENTS = {"16521000000101": [TEST_FILE_KEY, TEST_FILE_KEY]} MOCK_LG_DOCUMENT_REFERENCES = [TEST_FILE_KEY, TEST_FILE_KEY] -MOCK_BOTH_DOC_TYPES = {"ARF": [TEST_FILE_KEY, TEST_FILE_KEY], "LG": [TEST_FILE_KEY]} +MOCK_BOTH_DOC_TYPES = {"ARF": [TEST_FILE_KEY, TEST_FILE_KEY], "16521000000101": [TEST_FILE_KEY]} MOCK_NO_DOC_TYPE = {"": [TEST_FILE_KEY]} MOCK_VALID_LG_EVENT_BODY = { diff --git a/lambdas/tests/unit/services/test_document_reference_search_service.py b/lambdas/tests/unit/services/test_document_reference_search_service.py index 4a63f2cd7..096d3f83c 100644 --- a/lambdas/tests/unit/services/test_document_reference_search_service.py +++ b/lambdas/tests/unit/services/test_document_reference_search_service.py @@ -32,6 +32,8 @@ "id": "3d8683b9-1665-40d2-8499-6e8302d507ff", "fileSize": MOCK_FILE_SIZE, "version": "1", + "contentType": "application/pdf", + "documentSnomedCodeType": SnomedCodes.LLOYD_GEORGE.value.code, } diff --git a/lambdas/tests/unit/utils/decorators/conftest.py b/lambdas/tests/unit/utils/decorators/conftest.py index 15f820120..6f26a4efd 100644 --- a/lambdas/tests/unit/utils/decorators/conftest.py +++ b/lambdas/tests/unit/utils/decorators/conftest.py @@ -49,7 +49,7 @@ def valid_id_and_arf_doctype_event(): def valid_id_and_lg_doctype_event(): api_gateway_proxy_event = { "httpMethod": "GET", - "queryStringParameters": {"patientId": "9000000009", "docType": "LG"}, + "queryStringParameters": {"patientId": "9000000009", "docType": "16521000000101"}, } return api_gateway_proxy_event @@ -58,7 +58,7 @@ def valid_id_and_lg_doctype_event(): def valid_id_and_both_doctype_event(): api_gateway_proxy_event = { "httpMethod": "GET", - "queryStringParameters": {"patientId": "9000000009", "docType": "LG,ARF"}, + "queryStringParameters": {"patientId": "9000000009", "docType": "16521000000101,ARF"}, } return api_gateway_proxy_event diff --git a/lambdas/tests/unit/utils/test_document_type_utils.py b/lambdas/tests/unit/utils/test_document_type_utils.py index f0f337cb3..864ca1982 100644 --- a/lambdas/tests/unit/utils/test_document_type_utils.py +++ b/lambdas/tests/unit/utils/test_document_type_utils.py @@ -6,10 +6,10 @@ @pytest.mark.parametrize( "value", [ - "LG, ARF", - "ARF,LG", - " ARF, LG", - "LG , ARF", + "16521000000101, ARF", + "ARF,16521000000101", + " ARF, 16521000000101", + "16521000000101 , ARF", ], ) def test_extract_document_type_both(value): @@ -23,8 +23,8 @@ def test_extract_document_type_both(value): @pytest.mark.parametrize( "value", [ - "LG ", - " LG", + "16521000000101 ", + " 16521000000101", ], ) def test_extract_document_type_lg(value): @@ -56,11 +56,11 @@ def test_extract_document_type_arf(value): ("ARF", [SupportedDocumentTypes.ARF]), ("ARF ", [SupportedDocumentTypes.ARF]), (" ARF", [SupportedDocumentTypes.ARF]), - ("LG", [SupportedDocumentTypes.LG]), - ("LG ", [SupportedDocumentTypes.LG]), - (" LG", [SupportedDocumentTypes.LG]), - (" ARF, LG ", [SupportedDocumentTypes.ARF, SupportedDocumentTypes.LG]), - (" LG , ARF ", [SupportedDocumentTypes.LG, SupportedDocumentTypes.ARF]), + ("16521000000101", [SupportedDocumentTypes.LG]), + ("16521000000101 ", [SupportedDocumentTypes.LG]), + (" 16521000000101", [SupportedDocumentTypes.LG]), + (" ARF, 16521000000101 ", [SupportedDocumentTypes.ARF, SupportedDocumentTypes.LG]), + (" 16521000000101 , ARF ", [SupportedDocumentTypes.LG, SupportedDocumentTypes.ARF]), ], ) def test_extract_document_type_as_enum(value, expected):