From 2a9b3ed67a0d25f8107d3315215b6a74a255c8d0 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Tue, 15 Aug 2023 22:23:53 +0530 Subject: [PATCH 01/43] (test): adds test for `contact-details.component.tsx` (#779) * adds test for contact-details.component.tsx * fixed mocking data --- .../__mocks__/address.mock.ts | 8 ++ .../contact-details/contact-details.test.tsx | 82 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 packages/esm-patient-search-app/__mocks__/address.mock.ts create mode 100644 packages/esm-patient-search-app/src/patient-search-page/patient-banner/contact-details/contact-details.test.tsx diff --git a/packages/esm-patient-search-app/__mocks__/address.mock.ts b/packages/esm-patient-search-app/__mocks__/address.mock.ts new file mode 100644 index 000000000..3787aa428 --- /dev/null +++ b/packages/esm-patient-search-app/__mocks__/address.mock.ts @@ -0,0 +1,8 @@ +export const mockAddress = { + postalCode: '12345', + address1: '123 Main St', + cityVillage: 'City', + stateProvince: 'State', + country: 'Country', + preferred: true, +}; diff --git a/packages/esm-patient-search-app/src/patient-search-page/patient-banner/contact-details/contact-details.test.tsx b/packages/esm-patient-search-app/src/patient-search-page/patient-banner/contact-details/contact-details.test.tsx new file mode 100644 index 000000000..dad68a7f3 --- /dev/null +++ b/packages/esm-patient-search-app/src/patient-search-page/patient-banner/contact-details/contact-details.test.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import ContactDetails from './contact-details.component'; +import { useRelationships } from './relationships.resource'; +import { usePatientContactAttributes } from '../hooks/usePatientAttributes'; +import { mockAddress } from '../../../../__mocks__/address.mock'; + +const mockUseRelationships = useRelationships as jest.Mock; +const mockUsePatientContactAttributes = usePatientContactAttributes as jest.Mock; + +const mockContactAttributes = [ + { attributeType: { display: 'Email' }, value: 'test@example.com', uuid: '24252571-dd5a-11e6-9d9c-0242ac152222' }, +]; + +const mockRelationships = [ + { + uuid: '24252571-dd5a-11e6-9d9c-0242ac150002', + display: 'John Doe', + relationshipType: 'Family', + relativeAge: 30, + }, +]; + +jest.mock('./relationships.resource'); +jest.mock('../hooks/usePatientAttributes'); + +mockUsePatientContactAttributes.mockReturnValue({ + isLoading: false, + contactAttributes: mockContactAttributes, +}); + +mockUseRelationships.mockReturnValue({ isLoading: false, data: mockRelationships }); + +describe('ContactDetails', () => { + beforeEach(() => { + mockUsePatientContactAttributes.mockReturnValue({ + isLoading: false, + contactAttributes: mockContactAttributes, + }); + + mockUseRelationships.mockReturnValue({ isLoading: false, data: mockRelationships }); + }); + it('renders address', () => { + render(); + + expect(screen.getByText('Address')).toBeInTheDocument(); + expect(screen.getByText('12345')).toBeInTheDocument(); + expect(screen.getByText('123 Main St')).toBeInTheDocument(); + expect(screen.getByText('City')).toBeInTheDocument(); + expect(screen.getByText('State')).toBeInTheDocument(); + expect(screen.getByText('Country')).toBeInTheDocument(); + }); + + it('renders contact attributes', async () => { + render(); + + expect(screen.getByText('Contact Details')).toBeInTheDocument(); + expect(screen.getByText('Email : test@example.com')); + }); + + it('renders relationships', async () => { + render(); + + expect(screen.getByText('Relationships')).toBeInTheDocument(); + expect(screen.getByText('John Doe')).toBeInTheDocument(); + }); + + it('renders skeleton when loading', async () => { + mockUsePatientContactAttributes.mockReturnValue({ + isLoading: true, + contactAttributes: [], + }); + + mockUseRelationships.mockReturnValue({ isLoading: true, data: [] }); + + render(); + + expect(screen.getByText('Contact Details')).toBeInTheDocument(); + expect(screen.queryByText('John Doe')).not.toBeInTheDocument(); + expect(screen.queryByText('Email : test@example.com')).not.toBeInTheDocument(); + }); +}); From 3eeffc171727d0ef74c3ac2bffd3ba01771091ff Mon Sep 17 00:00:00 2001 From: Piumal Rathnayake Date: Wed, 16 Aug 2023 09:09:14 +0530 Subject: [PATCH 02/43] O3-2241: Use pre-filled docker images for running e2e tests (#753) * O3-2241: Use pre-filled docker images for running e2e tests * Remove unwanted files * Update docker-compose.yml * Combine onPush and onPr jobs into a single job * Change job name * Update e2e/support/github/docker-compose.yml * Update e2e/support/github/docker-compose.yml --- .github/workflows/docker/docker-compose.yml | 39 -------------- .github/workflows/e2e.yml | 57 +-------------------- e2e/support/github/docker-compose.yml | 33 ++---------- 3 files changed, 5 insertions(+), 124 deletions(-) delete mode 100644 .github/workflows/docker/docker-compose.yml diff --git a/.github/workflows/docker/docker-compose.yml b/.github/workflows/docker/docker-compose.yml deleted file mode 100644 index 23c3b5a43..000000000 --- a/.github/workflows/docker/docker-compose.yml +++ /dev/null @@ -1,39 +0,0 @@ -# This docker compose file is used to create a backend environment for the e2e.yml workflow. -version: "3.7" - -services: - backend: - image: openmrs/openmrs-reference-application-3-backend:${TAG:-nightly} - depends_on: - - db - environment: - OMRS_CONFIG_MODULE_WEB_ADMIN: "true" - OMRS_CONFIG_AUTO_UPDATE_DATABASE: "true" - OMRS_CONFIG_CREATE_TABLES: "true" - OMRS_CONFIG_CONNECTION_SERVER: db - OMRS_CONFIG_CONNECTION_DATABASE: openmrs - OMRS_CONFIG_CONNECTION_USERNAME: ${OPENMRS_DB_USER:-openmrs} - OMRS_CONFIG_CONNECTION_PASSWORD: ${OPENMRS_DB_PASSWORD:-openmrs} - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/openmrs"] - timeout: 5s - volumes: - - openmrs-data:/openmrs/data - ports: - - 9000:8080 - - # MariaDB - db: - image: mariadb:10.8.2 - command: "mysqld --character-set-server=utf8 --collation-server=utf8_general_ci" - environment: - MYSQL_DATABASE: openmrs - MYSQL_USER: ${OPENMRS_DB_USER:-openmrs} - MYSQL_PASSWORD: ${OPENMRS_DB_PASSWORD:-openmrs} - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-openmrs} - volumes: - - db-data:/var/lib/mysql - -volumes: - openmrs-data: ~ - db-data: ~ diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3a5ae0834..27a6f2f8d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -9,8 +9,7 @@ on: - main jobs: - testOnPR: - if: ${{ github.event_name == 'pull_request' }} + main: runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -62,57 +61,3 @@ jobs: name: playwright-report path: playwright-report/ retention-days: 30 - - testOnPush: - if: ${{ github.event_name == 'push' }} - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - - name: Copy test environment variables - run: cp example.env .env - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "18.x" - - - name: Cache dependencies - id: cache - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - - - name: Install dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: yarn install --immutable - - - name: Install Playwright Browsers - run: npx playwright install chromium --with-deps - - - name: Build apps - run: yarn turbo run build --color --concurrency=5 - - - name: Run dev server - run: bash e2e/support/github/run-e2e-docker-env.sh - - - name: Wait for OpenMRS instance to start - run: while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8080/openmrs/login.htm)" != "200" ]]; do sleep 10; done - - - name: Run E2E tests - run: yarn playwright test - - - name: Stop dev server - if: "!cancelled()" - run: docker stop $(docker ps -a -q) - - - name: Upload report - uses: actions/upload-artifact@v3 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 diff --git a/e2e/support/github/docker-compose.yml b/e2e/support/github/docker-compose.yml index 0c49fd169..d230464f4 100644 --- a/e2e/support/github/docker-compose.yml +++ b/e2e/support/github/docker-compose.yml @@ -1,4 +1,5 @@ -# This docker compose file is used to create a backend environment for the e2e.yml workflow. +# This docker compose file is used to create a backend environment for the e2e.yml workflow. +# The images are pre-filled with data so that the backend environment can be started within a short time. version: "3.7" services: @@ -15,35 +16,9 @@ services: API_URL: /openmrs backend: - image: openmrs/openmrs-reference-application-3-backend:${TAG:-nightly} + image: openmrs/openmrs-reference-application-3-backend:nightly-with-data depends_on: - db - environment: - OMRS_CONFIG_MODULE_WEB_ADMIN: "true" - OMRS_CONFIG_AUTO_UPDATE_DATABASE: "true" - OMRS_CONFIG_CREATE_TABLES: "true" - OMRS_CONFIG_CONNECTION_SERVER: db - OMRS_CONFIG_CONNECTION_DATABASE: openmrs - OMRS_CONFIG_CONNECTION_USERNAME: ${OPENMRS_DB_USER:-openmrs} - OMRS_CONFIG_CONNECTION_PASSWORD: ${OPENMRS_DB_PASSWORD:-openmrs} - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/openmrs"] - timeout: 5s - volumes: - - openmrs-data:/openmrs/data - # MariaDB db: - image: mariadb:10.8.2 - command: "mysqld --character-set-server=utf8 --collation-server=utf8_general_ci" - environment: - MYSQL_DATABASE: openmrs - MYSQL_USER: ${OPENMRS_DB_USER:-openmrs} - MYSQL_PASSWORD: ${OPENMRS_DB_PASSWORD:-openmrs} - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-openmrs} - volumes: - - db-data:/var/lib/mysql - -volumes: - openmrs-data: ~ - db-data: ~ + image: openmrs/openmrs-reference-application-3-db:nightly-with-data From 1950921a671ccf69b56a0d9944b0896e8c06c72b Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Fri, 18 Aug 2023 16:42:25 +0530 Subject: [PATCH 03/43] (test) O3-2081: Fix tests for the `PatientRegistration` component (#709) --- .../input/basic-input/input/input.test.tsx | 135 ----- .../patient-registration.test.tsx | 479 +++++++++--------- 2 files changed, 244 insertions(+), 370 deletions(-) diff --git a/packages/esm-patient-registration-app/src/patient-registration/input/basic-input/input/input.test.tsx b/packages/esm-patient-registration-app/src/patient-registration/input/basic-input/input/input.test.tsx index 532c8d971..779493650 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/input/basic-input/input/input.test.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/input/basic-input/input/input.test.tsx @@ -4,36 +4,6 @@ import userEvent from '@testing-library/user-event'; import { Formik, Form } from 'formik'; import { Input } from './input.component'; -describe.skip('number input', () => { - const setupInput = async () => { - render( - -
- -
-
, - ); - return screen.getByLabelText('Number (optional)') as HTMLInputElement; - }; - - it('exists', async () => { - const input = await setupInput(); - expect(input.type).toEqual('number'); - }); - - it('can input data', async () => { - const user = userEvent.setup(); - - const input = await setupInput(); - const expected = 1; - - await user.type(input, expected.toString()); - await user.tab(); - - expect(input.valueAsNumber).toEqual(expected); - }); -}); - describe('text input', () => { const setupInput = async () => { render( @@ -100,108 +70,3 @@ describe('text input', () => { expect(screen.getByLabelText('Text (optional)')).toBeInTheDocument(); }); }); - -describe.skip('telephone number input', () => { - const setupInput = async () => { - render( - -
- -
-
, - ); - return screen.getByLabelText('telephoneNumber') as HTMLInputElement; - }; - - it('exists', async () => { - const input = await setupInput(); - expect(input.type).toEqual('tel'); - }); - - it('can input data', async () => { - const user = userEvent.setup(); - - const input = await setupInput(); - const expected = '0800001066'; - - await user.type(input, expected); - await user.tab(); - - expect(input.value).toEqual(expected); - }); -}); - -describe.skip('date input', () => { - const setupInput = async () => { - render( - -
- -
-
, - ); - return screen.getByLabelText('date') as HTMLInputElement; - }; - - it('exists', async () => { - const input = await setupInput(); - expect(input.type).toEqual('date'); - }); - - it('can input data', async () => { - const user = userEvent.setup(); - - const input = await setupInput(); - const expected = '1990-09-10'; - - await user.type(input, expected); - await user.tab(); - - expect(input.value).toEqual(expected); - }); -}); - -describe.skip('checkbox input', () => { - const setupInput = async () => { - render( - -
- -
-
, - ); - return screen.getByLabelText('checkbox') as HTMLInputElement; - }; - - it('exists', async () => { - const input = await setupInput(); - expect(input.type).toEqual('checkbox'); - }); - - it('can input data', async () => { - const user = userEvent.setup(); - const input = await setupInput(); - - const expected = true; - - await user.click(input); - await user.tab(); - - expect(input.checked).toEqual(expected); - }); - - it('can update data', async () => { - const user = userEvent.setup(); - const input = await setupInput(); - - const expected = false; - - await user.click(input); - await user.tab(); - - await user.click(input); - await user.tab(); - - expect(input.checked).toEqual(expected); - }); -}); diff --git a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.test.tsx b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.test.tsx index ded64f6bf..32928085f 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/patient-registration.test.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/patient-registration.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; +import { BrowserRouter as Router, useParams } from 'react-router-dom'; +import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { showToast, useConfig, usePatient } from '@openmrs/esm-framework'; -import FormManager from './form-manager'; +import { FormManager } from './form-manager'; import { saveEncounter, savePatient } from './patient-registration.resource'; import { Encounter } from './patient-registration-types'; import { Resources, ResourcesContext } from '../offline.resources'; @@ -36,6 +36,7 @@ jest.mock('react-router-dom', () => ({ pathname: 'openmrs/spa/patient-registration', }), useHistory: () => [], + useParams: jest.fn().mockReturnValue({ patientUuid: undefined }), })); jest.mock('./patient-registration.resource', () => { @@ -167,264 +168,272 @@ const fillRequiredFields = async () => { await user.type(familyNameInput, 'Gaihre'); await user.clear(dateOfBirthInput); await user.type(dateOfBirthInput, '02/08/1993'); - fireEvent.blur(dateOfBirthInput); - fireEvent.click(genderInput); + user.click(genderInput); }; -describe('patient registration', () => { - beforeEach(() => { - mockedUseConfig.mockReturnValue(mockOpenmrsConfig); - mockedSavePatient.mockReturnValue({ data: { uuid: 'new-pt-uuid' }, ok: true }); - mockedSaveEncounter.mockClear(); - mockedShowToast.mockClear(); - }); +describe('patient registration component', () => { + describe('when registering a new patient', () => { + beforeEach(() => { + mockedUseConfig.mockReturnValue(mockOpenmrsConfig); + mockedSavePatient.mockReturnValue({ data: { uuid: 'new-pt-uuid' }, ok: true }); + mockedSaveEncounter.mockClear(); + mockedShowToast.mockClear(); + jest.clearAllMocks(); + }); - it.only('renders without crashing', () => { - render( - - - - - , - , - ); - }); + it('renders without crashing', () => { + render( + + + + + , + , + ); + }); - it('has the expected sections', async () => { - render( - - - - - , - ); - await waitFor(() => expect(screen.getByLabelText(/Demographics Section/)).not.toBeNull()); - expect(screen.getByLabelText(/Contact Info Section/)).not.toBeNull(); - }); + it('has the expected sections', async () => { + render( + + + + + , + ); + await waitFor(() => expect(screen.getByLabelText(/Demographics Section/)).not.toBeNull()); + expect(screen.getByLabelText(/Contact Info Section/)).not.toBeNull(); + }); - it('saves the patient without extra info', async () => { - const user = userEvent.setup(); - - render( - - - - - , - ); - - await fillRequiredFields(); - await user.click(await screen.findByText('Register Patient')); - await waitFor(() => { - expect(mockedSavePatient).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - identifiers: [], //TODO when the identifer story is finished: { identifier: '', identifierType: '05a29f94-c0ed-11e2-94be-8c13b969e334', location: '' } - // identifiers: [{ identifier: '', identifierType: '05a29f94-c0ed-11e2-94be-8c13b969e334', location: '' }], - person: { - addresses: [{}], - attributes: [], - birthdate: '1993-8-2', - birthdateEstimated: false, - gender: 'M', - names: [{ givenName: 'Paul', middleName: '', familyName: 'Gaihre', preferred: true, uuid: undefined }], - dead: false, - uuid: expect.anything(), - }, - uuid: expect.anything(), - }), - undefined, + it('saves the patient without extra info', async () => { + const user = userEvent.setup(); + + render( + + + + + , ); + + await fillRequiredFields(); + await user.click(await screen.findByText('Register Patient')); + await waitFor(() => { + expect(mockedSavePatient).toHaveBeenCalledWith( + expect.objectContaining({ + identifiers: [], //TODO when the identifer story is finished: { identifier: '', identifierType: '05a29f94-c0ed-11e2-94be-8c13b969e334', location: '' }, + person: { + addresses: expect.arrayContaining([expect.any(Object)]), + attributes: [], + birthdate: '1993-8-2', + birthdateEstimated: false, + gender: 'M', + names: [{ givenName: 'Paul', middleName: '', familyName: 'Gaihre', preferred: true, uuid: undefined }], + dead: false, + uuid: expect.anything(), + }, + uuid: expect.anything(), + }), + undefined, + ); + }); }); - }); - it('should not save the patient if validation fails', async () => { - const user = userEvent.setup(); + it('should not save the patient if validation fails', async () => { + const user = userEvent.setup(); - const mockedSavePatientForm = jest.fn(); - render( - - - - - , - ); + const mockedSavePatientForm = jest.fn(); + render( + + + + + , + ); - const givenNameInput = (await screen.findByLabelText('First Name')) as HTMLInputElement; + const givenNameInput = (await screen.findByLabelText('First Name')) as HTMLInputElement; - await user.type(givenNameInput, ''); - await user.click(screen.getByText('Register Patient')); + await user.type(givenNameInput, '5'); + await user.click(screen.getByText('Register Patient')); - expect(mockedSavePatientForm).not.toHaveBeenCalled(); - }); + expect(mockedSavePatientForm).not.toHaveBeenCalled(); + }); - it.skip('edits patient demographics', async () => { - const user = userEvent.setup(); + it('renders and saves registration obs', async () => { + const user = userEvent.setup(); - mockedSavePatient.mockResolvedValue({}); + mockedSaveEncounter.mockResolvedValue({}); + mockedUseConfig.mockReturnValue(configWithObs); - mockedUsePatient.mockReturnValueOnce({ - isLoading: false, - patient: mockPatient, - patientUuid: mockPatient.id, - error: null, + render( + + + + + , + ); + + await fillRequiredFields(); + const customSection = screen.getByLabelText('Custom Section'); + const weight = within(customSection).getByLabelText('Weight (kg) (optional)'); + await user.type(weight, '50'); + const complaint = within(customSection).getByLabelText('Chief Complaint (optional)'); + await user.type(complaint, 'sad'); + const nationality = within(customSection).getByLabelText('Nationality'); + await user.selectOptions(nationality, 'USA'); + + await user.click(screen.getByText('Register Patient')); + + await waitFor(() => expect(mockedSavePatient).toHaveBeenCalled()); + await waitFor(() => + expect(mockedSaveEncounter).toHaveBeenCalledWith( + expect.objectContaining>({ + encounterType: 'reg-enc-uuid', + patient: 'new-pt-uuid', + obs: [ + { concept: 'weight-uuid', value: 50 }, + { concept: 'chief-complaint-uuid', value: 'sad' }, + { concept: 'nationality-uuid', value: 'usa' }, + ], + }), + ), + ); }); - render( - - - - - , - ); - - const givenNameInput = screen.getByLabelText(/First Name/) as HTMLInputElement; - const familyNameInput = screen.getByLabelText(/Family Name/) as HTMLInputElement; - const middleNameInput = screen.getByLabelText(/Middle Name/) as HTMLInputElement; - const dateOfBirthInput = screen.getByLabelText('Date of Birth') as HTMLInputElement; - const address1 = screen.getByLabelText('Location.address1') as HTMLInputElement; - - // assert initial values - expect(givenNameInput.value).toBe('John'); - expect(familyNameInput.value).toBe('Wilson'); - expect(middleNameInput.value).toBeFalsy(); - expect(dateOfBirthInput.value).toBe('04/04/1972'); - - // do some edits - await user.clear(givenNameInput); - await user.clear(middleNameInput); - await user.clear(familyNameInput); - await user.type(givenNameInput, 'Eric'); - await user.type(middleNameInput, 'Johnson'); - await user.type(familyNameInput, 'Smith'); - await user.type(address1, 'Bom Jesus Street'); - await user.click(screen.getByText('Update Patient')); - - expect(mockedSavePatient).toHaveBeenCalledWith( - expect.anything(), - { - uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e', - identifiers: [ - { - uuid: '1f0ad7a1-430f-4397-b571-59ea654a52db', - identifier: '100GEJ', - identifierType: 'e5af9a9c-ff9d-486d-900c-5fbf66a5ba3c', - preferred: true, - }, - { - uuid: '1f0ad7a1-430f-4397-b571-59ea654a52db', - identifier: '100732HE', - identifierType: '3ff0063c-dd45-4d98-8af4-0c094f26166c', - preferred: false, - }, - ], - person: { - addresses: [ - { - address1: 'Bom Jesus Street', - address2: '', - cityVillage: 'City0351', - country: 'Country0351', - postalCode: '60351', - stateProvince: 'State0351tested', - }, - ], - attributes: [], - birthdate: new Date('1972-04-04'), - birthdateEstimated: false, - gender: 'M', - names: [ - { - uuid: 'efdb246f-4142-4c12-a27a-9be60b9592e9', - givenName: 'Eric', - middleName: 'Johnson', - familyName: 'Smith', - preferred: true, - }, - ], - dead: false, - uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e', - }, - }, - '8673ee4f-e2ab-4077-ba55-4980f408773e', - ); - }); + it('retries saving registration obs after a failed attempt', async () => { + const user = userEvent.setup(); - it('renders and saves registration obs', async () => { - const user = userEvent.setup(); - - mockedSaveEncounter.mockResolvedValue({}); - mockedUseConfig.mockReturnValue(configWithObs); - - render( - - - - - , - ); - - await fillRequiredFields(); - const customSection = screen.getByLabelText('Custom Section'); - const weight = within(customSection).getByLabelText('Weight (kg)'); - await user.type(weight, '50'); - const complaint = within(customSection).getByLabelText('Chief Complaint'); - await user.type(complaint, 'sad'); - const nationality = within(customSection).getByLabelText('Nationality'); - await user.selectOptions(nationality, 'USA'); - - await user.click(screen.getByText('Register Patient')); - - await waitFor(() => expect(mockedSavePatient).toHaveBeenCalled()); - await waitFor(() => - expect(mockedSaveEncounter).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining>({ - encounterType: 'reg-enc-uuid', - patient: 'new-pt-uuid', - obs: [ - { concept: 'weight-uuid', value: 50 }, - { concept: 'chief-complaint-uuid', value: 'sad' }, - { concept: 'nationality-uuid', value: 'usa' }, - ], - }), - ), - ); - }); + mockedUseConfig.mockReturnValue(configWithObs); - it('retries saving registration obs after a failed attempt', async () => { - const user = userEvent.setup(); + render( + + + + + , + ); - mockedUseConfig.mockReturnValue(configWithObs); + await fillRequiredFields(); + const customSection = screen.getByLabelText('Custom Section'); + const weight = within(customSection).getByLabelText('Weight (kg) (optional)'); + await user.type(weight, '-999'); - render( - - - - - , - ); + mockedSaveEncounter.mockRejectedValue({ status: 400, responseBody: { error: { message: 'an error message' } } }); - await fillRequiredFields(); - const customSection = screen.getByLabelText('Custom Section'); - const weight = within(customSection).getByLabelText('Weight (kg)'); - await user.type(weight, '-999'); + await user.click(screen.getByText('Register Patient')); - mockedSaveEncounter.mockRejectedValue({ status: 400, responseBody: { error: { message: 'an error message' } } }); + await waitFor(() => expect(mockedSavePatient).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(mockedSaveEncounter).toHaveBeenCalledTimes(1)); + await waitFor(() => + expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ description: 'an error message' })), + ); + + mockedSaveEncounter.mockResolvedValue({}); + + await user.click(screen.getByText('Register Patient')); + await waitFor(() => expect(mockedSavePatient).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(mockedSaveEncounter).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }))); + }); + }); - await user.click(screen.getByText('Register Patient')); + describe('when updating an existing patient details', () => { + beforeEach(() => { + mockedUseConfig.mockReturnValue(mockOpenmrsConfig); + mockedSavePatient.mockReturnValue({ data: { uuid: 'new-pt-uuid' }, ok: true }); + mockedSaveEncounter.mockClear(); + mockedShowToast.mockClear(); + jest.clearAllMocks(); + }); - await waitFor(() => expect(mockedSavePatient).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(mockedSaveEncounter).toHaveBeenCalledTimes(1)); - await waitFor(() => - expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ description: 'an error message' })), - ); + it('edits patient demographics', async () => { + const user = userEvent.setup(); - mockedSaveEncounter.mockResolvedValue({}); + mockedSavePatient.mockResolvedValue({}); - await user.click(screen.getByText('Register Patient')); - await waitFor(() => expect(mockedSavePatient).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(mockedSaveEncounter).toHaveBeenCalledTimes(2)); - await waitFor(() => expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }))); + const mockedUseParams = useParams as jest.Mock; + + mockedUseParams.mockReturnValue({ patientUuid: mockPatient.id }); + + mockedUsePatient.mockReturnValue({ + isLoading: false, + patient: mockPatient, + patientUuid: mockPatient.id, + error: null, + }); + + render( + + + + + , + ); + + const givenNameInput: HTMLInputElement = screen.getByLabelText(/First Name/); + const familyNameInput: HTMLInputElement = screen.getByLabelText(/Family Name/); + const middleNameInput: HTMLInputElement = screen.getByLabelText(/Middle Name/); + const dateOfBirthInput: HTMLInputElement = screen.getByLabelText('Date of Birth'); + const genderInput: HTMLInputElement = screen.getByLabelText(/Male/); + + // assert initial values + expect(givenNameInput.value).toBe('John'); + expect(familyNameInput.value).toBe('Wilson'); + expect(middleNameInput.value).toBeFalsy(); + expect(dateOfBirthInput.value).toBe('4/4/1972'); + expect(genderInput.value).toBe('Male'); + + // do some edits + await user.clear(givenNameInput); + await user.clear(middleNameInput); + await user.clear(familyNameInput); + await user.type(givenNameInput, 'Eric'); + await user.type(middleNameInput, 'Johnson'); + await user.type(familyNameInput, 'Smith'); + await user.click(screen.getByText('Update Patient')); + + await waitFor(() => + expect(mockedSavePatient).toHaveBeenCalledWith( + false, + { + '0': { + oldIdentificationNumber: '100732HE', + }, + '1': { + openMrsId: '100GEJ', + }, + addNameInLocalLanguage: undefined, + additionalFamilyName: '', + additionalGivenName: '', + additionalMiddleName: '', + address: {}, + birthdate: new Date('1972-04-04T00:00:00.000Z'), + birthdateEstimated: false, + deathCause: '', + deathDate: '', + familyName: 'Smith', + gender: 'Male', + givenName: 'Eric', + identifiers: {}, + isDead: false, + middleName: 'Johnson', + monthsEstimated: 0, + patientUuid: '8673ee4f-e2ab-4077-ba55-4980f408773e', + relationships: [], + telephoneNumber: '', + unidentifiedPatient: undefined, + yearsEstimated: 0, + }, + expect.anything(), + expect.anything(), + null, + undefined, + expect.anything(), + expect.anything(), + expect.anything(), + { patientSaved: false }, + expect.anything(), + ), + ); + }); }); }); From b308d8a6d377ef93ff957c4456ae4f5a274ad3fe Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:31:12 +0530 Subject: [PATCH 04/43] (test): Adds test for patient-search-bar.component.tsx (#777) --- .../patient-search-bar.test.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 packages/esm-patient-search-app/src/patient-search-bar/patient-search-bar.test.tsx diff --git a/packages/esm-patient-search-app/src/patient-search-bar/patient-search-bar.test.tsx b/packages/esm-patient-search-app/src/patient-search-bar/patient-search-bar.test.tsx new file mode 100644 index 000000000..dcb8e22d5 --- /dev/null +++ b/packages/esm-patient-search-app/src/patient-search-bar/patient-search-bar.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import PatientSearchBar from './patient-search-bar.component'; + +describe('PatientSearchBar', () => { + it('should render', () => { + render(); + + const searchInput = screen.getByPlaceholderText('Search for a patient by name or identifier number'); + + expect(searchInput).toBeInTheDocument(); + }); + + it('displays initial search term', () => { + const initialSearchTerm = 'John Doe'; + render(); + + const searchInput: HTMLInputElement = screen.getByPlaceholderText( + 'Search for a patient by name or identifier number', + ); + + expect(searchInput.value).toBe(initialSearchTerm); + }); + + it('calls onChange callback on input change', () => { + const onChangeMock = jest.fn(); + render(); + + const searchInput = screen.getByPlaceholderText('Search for a patient by name or identifier number'); + + fireEvent.change(searchInput, { target: { value: 'New Value' } }); + + expect(onChangeMock).toHaveBeenCalledWith('New Value'); + }); + + it('calls onClear callback on clear button click', () => { + const onClearMock = jest.fn(); + render(); + + const clearButton = screen.getByRole('button', { name: 'Clear' }); + + fireEvent.click(clearButton); + + expect(onClearMock).toHaveBeenCalled(); + }); + + it('calls onSubmit callback on form submission', () => { + const onSubmitMock = jest.fn(); + render(); + + const searchInput = screen.getByPlaceholderText('Search for a patient by name or identifier number'); + const searchButton = screen.getByRole('button', { name: 'Search' }); + + fireEvent.change(searchInput, { target: { value: 'Search Term' } }); + fireEvent.click(searchButton); + + expect(onSubmitMock).toHaveBeenCalledWith('Search Term'); + }); +}); From 0787a2f52489510ce03e0b38237755a219d46116 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Fri, 18 Aug 2023 18:21:19 +0530 Subject: [PATCH 05/43] (test) Add test for overlay.component.tsx (#788) --- .../esm-patient-list-app/src/overlay.test.tsx | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/esm-patient-list-app/src/overlay.test.tsx diff --git a/packages/esm-patient-list-app/src/overlay.test.tsx b/packages/esm-patient-list-app/src/overlay.test.tsx new file mode 100644 index 000000000..a9e2adf28 --- /dev/null +++ b/packages/esm-patient-list-app/src/overlay.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import Overlay from './overlay.component'; +import { useLayoutType, isDesktop } from '@openmrs/esm-framework'; + +const mockUseLayoutType = useLayoutType as jest.Mock; +const mockIsDesktop = isDesktop as jest.Mock; + +jest.mock('@openmrs/esm-framework'); + +describe('Overlay', () => { + it('renders the desktop version of the overlay', () => { + mockUseLayoutType.mockImplementation(() => 'desktop'); + render( + {}} header="Test Header"> + Overlay content + , + ); + + const headerContent = screen.getByText('Test Header'); + const closeButton = screen.getByRole('button', { name: 'Close overlay' }); + + expect(headerContent).toBeInTheDocument(); + expect(closeButton).toBeInTheDocument(); + }); + + it('renders the tablet version of the overlay', () => { + mockUseLayoutType.mockImplementation(() => 'tablet'); + mockIsDesktop.mockImplementation(() => false); + render( + {}} header="Test Header"> + Overlay content + , + ); + + const headerContent = screen.getByText('Test Header'); + const backButton = screen.getByRole('button', { name: 'Close overlay' }); + + expect(headerContent).toBeInTheDocument(); + expect(backButton).toBeInTheDocument(); + }); + + it('calls the close function when close button is clicked', () => { + const mockClose = jest.fn(); + mockUseLayoutType.mockImplementation(() => 'desktop'); + render( + + Overlay content + , + ); + + const closeButton = screen.getByRole('button', { name: 'Close overlay' }); + fireEvent.click(closeButton); + + expect(mockClose).toHaveBeenCalled(); + }); +}); From 0de48b13ba83cdda39a9794c10d95a5e44f0dac0 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Fri, 18 Aug 2023 18:33:07 +0530 Subject: [PATCH 06/43] (test): Add tests for `recent-patient-search.component.tsx` (#784) --- .../recent-patient-search.test.tsx | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 packages/esm-patient-search-app/src/compact-patient-search/recent-patient-search.test.tsx diff --git a/packages/esm-patient-search-app/src/compact-patient-search/recent-patient-search.test.tsx b/packages/esm-patient-search-app/src/compact-patient-search/recent-patient-search.test.tsx new file mode 100644 index 000000000..990ebf27a --- /dev/null +++ b/packages/esm-patient-search-app/src/compact-patient-search/recent-patient-search.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import RecentPatientSearch from './recent-patient-search.component'; +import { useConfig } from '@openmrs/esm-framework'; + +const mockedUseConfig = useConfig as jest.Mock; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), +})); + +describe('RecentPatientSearch', () => { + beforeEach(() => { + mockedUseConfig.mockReturnValue({ + defaultIdentifierTypes: ['identifier-type-1', 'identifier-type-2'], + search: { + patientResultUrl: '/patient/{{patientUuid}}/chart', + }, + }); + }); + + it('should render loading skeleton when isLoading is true', () => { + render(); + + const resultText = screen.queryByText(/recent search result/i); + expect(resultText).not.toBeInTheDocument(); + }); + + it('should render error message when fetchError is true', () => { + const error = new Error('Error'); + render(); + const errorMessage = screen.getByText('Error'); + + expect(errorMessage).toBeInTheDocument(); + }); + + it('should render search results when searchResults contain data', () => { + const mockSearchResults = [ + { + uuid: 'patient-1', + identifiers: [{ identifier: '123', identifierType: { uuid: 'identifier-type-1' } }], + person: { + personName: { + givenName: 'John', + middleName: 'Doe', + }, + }, + }, + ]; + + render(); + const resultText = screen.getByText(/recent search result/i); + + expect(resultText).toBeInTheDocument(); + }); + + it('should render empty result message when searchResults is empty', () => { + render(); + const emptyResultText = screen.getByText(/no patient charts were found/i); + + expect(emptyResultText).toBeInTheDocument(); + }); +}); From 8652cc744f2d8b863172a37e7e969bcb1c505ce3 Mon Sep 17 00:00:00 2001 From: Dennis Kigen Date: Fri, 18 Aug 2023 18:02:34 +0300 Subject: [PATCH 07/43] (docs) Add notes for how to run tests using turbo (#795) --- README.md | 44 +++++++++++++++++++ packages/esm-active-visits-app/package.json | 4 +- packages/esm-outpatient-app/package.json | 4 +- packages/esm-patient-list-app/package.json | 4 +- .../esm-patient-registration-app/package.json | 4 +- packages/esm-patient-search-app/package.json | 4 +- 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1c275725e..e8061eacf 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,50 @@ Please read our [contributing](http://o3-dev.docs.openmrs.org/#/getting_started/ ## Running tests +To run tests for all packages, run: + +```bash +yarn turbo test +``` + +To run tests in `watch` mode, run: + +```bash +yarn turbo test:watch +``` + +To run tests for a specific package, pass the package name to the `--filter` flag. For example, to run tests for `esm-patient-conditions-app`, run: + +```bash +yarn turbo test --filter="esm-patient-conditions-app" +``` + +To run a specific test file, run: + +```bash +yarn turbo test -- basic-search +``` + +The above command will only run tests in the file or files that match the provided string. + +You can also run the matching tests from above in watch mode by running: + +```bash +yarn turbo test:watch --basic-search +``` + +To generate a `coverage` report, run: + +```bash +yarn turbo coverage +``` + +By default, `turbo` will cache test runs. This means that re-running tests wihout changing any of the related files will return the cached logs from the last run. To bypass the cache, run tests with the `force` flag, as follows: + +```bash +yarn turbo test --force +``` + ### Unit tests To run unit tests, use: diff --git a/packages/esm-active-visits-app/package.json b/packages/esm-active-visits-app/package.json index f895249c8..709fd6ca2 100644 --- a/packages/esm-active-visits-app/package.json +++ b/packages/esm-active-visits-app/package.json @@ -14,8 +14,8 @@ "build": "webpack --mode production", "analyze": "webpack --mode=production --env.analyze=true", "lint": "cross-env TIMING=1 eslint src --ext ts,tsx", - "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests", - "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js", + "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color", + "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color", "coverage": "yarn test --coverage", "typescript": "tsc", "extract-translations": "i18next 'src/**/*.component.tsx'" diff --git a/packages/esm-outpatient-app/package.json b/packages/esm-outpatient-app/package.json index d2b742a6f..c45d2c611 100644 --- a/packages/esm-outpatient-app/package.json +++ b/packages/esm-outpatient-app/package.json @@ -14,8 +14,8 @@ "build": "webpack --mode production", "analyze": "webpack --mode=production --env.analyze=true", "lint": "cross-env TIMING=1 eslint src --ext ts,tsx", - "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests", - "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js", + "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color", + "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color", "coverage": "yarn test --coverage", "typescript": "tsc", "extract-translations": "i18next 'src/**/*.component.tsx'" diff --git a/packages/esm-patient-list-app/package.json b/packages/esm-patient-list-app/package.json index ce5e5e9fa..153d545c6 100644 --- a/packages/esm-patient-list-app/package.json +++ b/packages/esm-patient-list-app/package.json @@ -14,8 +14,8 @@ "build": "webpack --mode production", "analyze": "webpack --mode=production --env.analyze=true", "lint": "cross-env TIMING=1 eslint src --ext ts,tsx", - "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests", - "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js", + "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color", + "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color", "coverage": "yarn test --coverage", "typescript": "tsc", "extract-translations": "i18next 'src/**/*.component.tsx'" diff --git a/packages/esm-patient-registration-app/package.json b/packages/esm-patient-registration-app/package.json index 6c705d329..94d314d20 100644 --- a/packages/esm-patient-registration-app/package.json +++ b/packages/esm-patient-registration-app/package.json @@ -14,8 +14,8 @@ "build": "webpack --mode production", "analyze": "webpack --mode=production --env.analyze=true", "lint": "cross-env TIMING=1 eslint src --ext ts,tsx", - "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests", - "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js", + "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color", + "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color", "coverage": "yarn test --coverage", "typescript": "tsc", "extract-translations": "i18next 'src/**/*.component.tsx'" diff --git a/packages/esm-patient-search-app/package.json b/packages/esm-patient-search-app/package.json index 20c72205c..98dcb956f 100644 --- a/packages/esm-patient-search-app/package.json +++ b/packages/esm-patient-search-app/package.json @@ -14,8 +14,8 @@ "build": "webpack --mode production", "analyze": "webpack --mode=production --env.analyze=true", "lint": "cross-env TIMING=1 eslint src --ext ts,tsx", - "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests", - "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js", + "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color", + "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color", "coverage": "yarn test --coverage", "typescript": "tsc", "extract-translations": "i18next 'src/**/*.component.tsx'" From 16b5b41260ccfb2742602a1094e35668addb03db Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Sat, 19 Aug 2023 22:58:45 +0530 Subject: [PATCH 08/43] Add test for root.component.tsx (#794) --- packages/esm-patient-list-app/src/root.test.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/esm-patient-list-app/src/root.test.tsx diff --git a/packages/esm-patient-list-app/src/root.test.tsx b/packages/esm-patient-list-app/src/root.test.tsx new file mode 100644 index 000000000..74790cd1c --- /dev/null +++ b/packages/esm-patient-list-app/src/root.test.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import RootComponent from './root.component'; +window['getOpenmrsSpaBase'] = jest.fn().mockImplementation(() => '/'); + +describe('RootComponent', () => { + it('renders without crashing', () => { + render(); + }); +}); From bf79ae4d07466b01ef1e4b5d4916cadae0716972 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:02:07 +0530 Subject: [PATCH 09/43] (test) O3-2227/8: Add Test for `display-photo.component.tsx` and `edit-patient-details-button.component` (#751) --- .../src/widgets/display-photo.test.tsx | 37 +++++++++++++++++++ .../edit-patient-details-button.test.tsx | 36 ++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 packages/esm-patient-registration-app/src/widgets/display-photo.test.tsx create mode 100644 packages/esm-patient-registration-app/src/widgets/edit-patient-details-button.test.tsx diff --git a/packages/esm-patient-registration-app/src/widgets/display-photo.test.tsx b/packages/esm-patient-registration-app/src/widgets/display-photo.test.tsx new file mode 100644 index 000000000..7e9a16725 --- /dev/null +++ b/packages/esm-patient-registration-app/src/widgets/display-photo.test.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import DisplayPatientPhoto from './display-photo.component'; +import { mockPatient } from '../../../../__mocks__/appointments.mock'; + +jest.mock('../patient-registration/patient-registration.resource', () => ({ + usePatientPhoto: jest.fn().mockReturnValue({ data: { imageSrc: 'test-image-src' } }), +})); + +jest.mock('geopattern', () => ({ + generate: jest.fn().mockReturnValue({ + toDataUri: jest.fn().mockReturnValue('https://example.com'), + }), +})); + +const patientUuid = mockPatient.uuid; +const patientName = mockPatient.name; + +describe('DisplayPatientPhoto Component', () => { + it('should render the component with the patient photo and size should not be small', () => { + render(); + + const avatarImage = screen.getByTitle(`${patientName}`); + + expect(avatarImage).toBeInTheDocument(); + expect(avatarImage).toHaveAttribute('style', expect.stringContaining('width: 80px; height: 80px')); + }); + + it('should render the component with the patient photo and size should be small i.e. 48px', () => { + render(); + + const avatarImage = screen.getByTitle(`${patientName}`); + + expect(avatarImage).toBeInTheDocument(); + expect(avatarImage).toHaveAttribute('style', expect.stringContaining('width: 48px; height: 48px')); + }); +}); diff --git a/packages/esm-patient-registration-app/src/widgets/edit-patient-details-button.test.tsx b/packages/esm-patient-registration-app/src/widgets/edit-patient-details-button.test.tsx new file mode 100644 index 000000000..ee2496b9c --- /dev/null +++ b/packages/esm-patient-registration-app/src/widgets/edit-patient-details-button.test.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import EditPatientDetailsButton from './edit-patient-details-button.component'; +import { navigate } from '@openmrs/esm-framework'; +import { mockPatient } from '../../../../__mocks__/appointments.mock'; + +describe('EditPatientDetailsButton', () => { + const patientUuid = mockPatient.uuid; + it('should navigate to the edit page when clicked', () => { + const mockNavigate = navigate as jest.Mock; + + jest.mock('@openmrs/esm-framework', () => { + const originalModule = jest.requireActual('@openmrs/esm-framework'); + return { + ...originalModule, + }; + }); + + render(); + + const button = screen.getByRole('menuitem'); + fireEvent.click(button); + + expect(mockNavigate).toHaveBeenCalledWith({ to: expect.stringContaining(`/patient/${patientUuid}/edit`) }); + }); + + it('should call the onTransition function when provided', () => { + const onTransitionMock = jest.fn(); + render(); + + const button = screen.getByRole('menuitem'); + fireEvent.click(button); + + expect(onTransitionMock).toHaveBeenCalled(); + }); +}); From c1da91d32e66f1194554fb7f77b0779367ef0716 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:42:49 +0530 Subject: [PATCH 10/43] (test) Add tests for compact-patient-banner.component.tsx (#782) Co-authored-by: Piumal Rathnayake --- .../compact-patient-banner.component.tsx | 2 +- .../compact-patient-banner.test.tsx | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 packages/esm-patient-search-app/src/compact-patient-search/compact-patient-banner.test.tsx diff --git a/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-banner.component.tsx b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-banner.component.tsx index 96cda1a8e..99a6e7278 100644 --- a/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-banner.component.tsx +++ b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-banner.component.tsx @@ -135,7 +135,7 @@ const PatientSearchResults = React.forwardRef { return ( -
+
({ + ...jest.requireActual('@openmrs/esm-framework'), +})); + +describe('Compact Patient Search Results', () => { + beforeEach(() => { + mockedUseConfig.mockReturnValue({ + defaultIdentifierTypes: ['identifier-type-1', 'identifier-type-2'], + search: { + patientResultUrl: '/patient/{{patientUuid}}/chart', + }, + }); + }); + const patients = [ + { + uuid: 'patient-1', + person: { + personName: { + givenName: 'John', + middleName: 'Doe', + familyName: 'Smith', + }, + gender: 'M', + birthdate: '1990-01-01', + }, + identifiers: [{ identifier: '123', identifierType: { uuid: 'identifier-type-1' } }], + }, + ]; + + it('should render patient search results', () => { + render(); + + expect(screen.getByText('John Doe Smith')).toBeInTheDocument(); + + expect(screen.getByRole('img')).toBeInTheDocument(); + }); + + // Fix this test later + // it('should call selectPatientAction when a patient is clicked', () => { + // const selectPatientActionMock = jest.fn(); + // render(); + + // fireEvent.click(screen.getByText('John Doe Smith')); + // expect(selectPatientActionMock).toBeCalledWith(patients[0]); + // }); +}); From 589abf70cde920b2a27db46586e40e28ae56e894 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:45:16 +0530 Subject: [PATCH 11/43] (test) Add test for patient-list-action-button.test.tsx (#789) Co-authored-by: Piumal Rathnayake --- .../src/patient-list-action-button.test.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/esm-patient-list-app/src/patient-list-action-button.test.tsx diff --git a/packages/esm-patient-list-app/src/patient-list-action-button.test.tsx b/packages/esm-patient-list-app/src/patient-list-action-button.test.tsx new file mode 100644 index 000000000..719df99a2 --- /dev/null +++ b/packages/esm-patient-list-app/src/patient-list-action-button.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import PatientListActionButton from './patient-list-action-button.component'; +import { navigate, useLayoutType } from '@openmrs/esm-framework'; + +const mockedNavigate = navigate as jest.Mock; +const mockedUseLayoutType = useLayoutType as jest.Mock; + +jest.mock('@openmrs/esm-framework'); + +describe('PatientListActionButton', () => { + it('renders the button with the correct label', () => { + render(); + const button = screen.getByRole('button'); + const label = screen.getByText('Patient lists'); + + expect(button).toBeInTheDocument(); + expect(label).toBeInTheDocument(); + }); + + it('navigates to patient list page when clicked', () => { + mockedNavigate.mockImplementation(() => {}); + mockedUseLayoutType.mockImplementation(() => 'desktop'); + + render(); + const button = screen.getByRole('button'); + + fireEvent.click(button); + + expect(mockedNavigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/home/patient-lists' }); + }); + + it('renders tablet layout button when layout is tablet', () => { + mockedUseLayoutType.mockImplementation(() => 'tablet'); + + render(); + const button = screen.getByRole('button'); + const label = screen.getByText('Patient lists'); + + expect(button).toBeInTheDocument(); + expect(label).toBeInTheDocument(); + }); +}); From 3ece1e4c7704dd4564b88e4459f4c0736bde9f59 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:45:36 +0530 Subject: [PATCH 12/43] (test) Add test for patient-list-action-button.tsx (#789) Co-authored-by: Piumal Rathnayake From 070820b391b032cfecb52c8008bf4e463cd18bc5 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:48:58 +0530 Subject: [PATCH 13/43] (test) Add test for patient-list-list.component.tsx (#791) Co-authored-by: Piumal Rathnayake --- .../patient-list-list.test.tsx | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 packages/esm-patient-list-app/src/patient-list-list/patient-list-list.test.tsx diff --git a/packages/esm-patient-list-app/src/patient-list-list/patient-list-list.test.tsx b/packages/esm-patient-list-app/src/patient-list-list/patient-list-list.test.tsx new file mode 100644 index 000000000..eb477c9e3 --- /dev/null +++ b/packages/esm-patient-list-app/src/patient-list-list/patient-list-list.test.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { useLocation } from 'react-router-dom'; +import PatientListList from './patient-list-list.component'; + +const mockUseLocation = useLocation as jest.Mock; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn(), +})); + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + useSession: jest.fn(() => ({ + user: { + uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e', + }, + })), + navigate: jest.fn(), +})); + +describe('PatientListList', () => { + beforeEach(() => { + mockUseLocation.mockReturnValue({ + pathname: '/', + search: '', + hash: '', + state: null, + key: 'default', + }); + }); + + it('renders tabs and table correctly', () => { + render(); + + expect(screen.getByText('Starred lists')).toBeInTheDocument(); + expect(screen.getByText('System lists')).toBeInTheDocument(); + expect(screen.getByText('My lists')).toBeInTheDocument(); + expect(screen.getByText('All lists')).toBeInTheDocument(); + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + + it('clicking tabs updates the selected tab', () => { + render(); + + const systemListsTab = screen.getByRole('tab', { name: 'System lists' }); + expect(systemListsTab).toHaveAttribute('aria-selected', 'false'); + fireEvent.click(systemListsTab); + + expect(systemListsTab).toHaveAttribute('aria-selected', 'true'); + }); +}); From f734cf2df8fa5cfd9e10c3736e4877dec5dcdc66 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Wed, 23 Aug 2023 19:07:38 +0530 Subject: [PATCH 14/43] adds test for patient-search-page.component.tsx (#780) --- .../patient-search-page.test.tsx | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/esm-patient-search-app/src/patient-search-page/patient-search-page.test.tsx diff --git a/packages/esm-patient-search-app/src/patient-search-page/patient-search-page.test.tsx b/packages/esm-patient-search-app/src/patient-search-page/patient-search-page.test.tsx new file mode 100644 index 000000000..a359ac205 --- /dev/null +++ b/packages/esm-patient-search-app/src/patient-search-page/patient-search-page.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { isDesktop } from '@openmrs/esm-framework'; +import PatientSearchPageComponent from './patient-search-page.component'; + +const mockIsDesktop = isDesktop as jest.Mock; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + isDesktop: jest.fn(), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(() => ({ + page: 1, + })), + useLocation: jest.fn(), + useSearchParams: jest.fn(() => [ + { + get: jest.fn(() => 'John'), + }, + ]), +})); + +describe('PatientSearchPageComponent', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render advanced search component on desktop layout', () => { + mockIsDesktop.mockReturnValue(true); + render(); + + const applyBtn = screen.getByRole('button', { name: 'Apply' }); + const resetBtn = screen.getByRole('button', { name: /Reset/i }); + + expect(applyBtn).toBeInTheDocument(); + expect(resetBtn).toBeInTheDocument(); + }); + + it('should render patient search overlay on tablet layout', () => { + mockIsDesktop.mockReturnValue(false); + + render(); + + const searchBtn = screen.getByRole('button', { name: 'Search' }); + expect(searchBtn).toBeInTheDocument(); + }); +}); From 06c0e7c4dd4c1b9bee9b89ce9ebe43fa422e62e4 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Wed, 23 Aug 2023 19:12:26 +0530 Subject: [PATCH 15/43] (test): Add tests for `compact-patient-search.component.tsx` (#785) * Add tests for compact-patient-search.component.tsx * removed the skipped test' --- .../compact-patient-search.test.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.test.tsx diff --git a/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.test.tsx b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.test.tsx new file mode 100644 index 000000000..fe9afc564 --- /dev/null +++ b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { render, fireEvent, waitFor, screen } from '@testing-library/react'; +import CompactPatientSearchComponent from './compact-patient-search.component'; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + useSession: jest.fn(() => ({ + ...jest.requireActual('@openmrs/esm-framework').useSession(), + sessionLocation: { + uuid: 'location-uuid', + }, + })), + useConfig: jest.fn(() => ({ + ...jest.requireActual('@openmrs/esm-framework').useConfig(), + search: { + patientResultUrl: '/patient/{{patientUuid}}/chart', + }, + })), +})); + +describe('CompactPatientSearchComponent', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByRole('searchbox')).toBeInTheDocument(); + }); + + it('updates search term on input change', async () => { + render(); + const searchInput: HTMLInputElement = screen.getByRole('searchbox'); + + fireEvent.change(searchInput, { target: { value: 'John' } }); + + await waitFor(() => { + expect(searchInput.value).toBe('John'); + }); + }); + + it('clears search term on clear button click', async () => { + render(); + const searchInput: HTMLInputElement = screen.getByRole('searchbox'); + const clearButton = screen.getByRole('button', { name: 'Clear' }); + + fireEvent.change(searchInput, { target: { value: 'John' } }); + fireEvent.click(clearButton); + + await waitFor(() => { + expect(searchInput.value).toBe(''); + }); + }); + + it('renders search results when search term is not empty', async () => { + render(); + const searchInput = screen.getByRole('searchbox'); + + fireEvent.change(searchInput, { target: { value: 'John' } }); + + await waitFor(() => { + const searchResultsContainer = screen.getByTestId('floatingSearchResultsContainer'); + expect(searchResultsContainer).toBeInTheDocument(); + }); + }); + + it('renders recent patient search when search term is empty', async () => { + render(); + const searchInput = screen.getByRole('searchbox'); + + fireEvent.change(searchInput, { target: { value: '' } }); + + await waitFor(() => { + const searchResultsContainer = screen.getByTestId('floatingSearchResultsContainer'); + expect(searchResultsContainer).toBeInTheDocument(); + }); + }); +}); From 7d8eb303a437ee858f03ba7effc200c71d6e01cf Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Wed, 23 Aug 2023 19:14:58 +0530 Subject: [PATCH 16/43] (test) Add tests for `add-patient-to-patient-list-menu-item.component.tsx` (#787) * Add tests for add-patient-to-patient-list-menu-item.component.tsx * changed uuid --- ...patient-to-patient-list-menu-item.test.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/esm-patient-list-app/src/add-patient-to-patient-list-menu-item.test.tsx diff --git a/packages/esm-patient-list-app/src/add-patient-to-patient-list-menu-item.test.tsx b/packages/esm-patient-list-app/src/add-patient-to-patient-list-menu-item.test.tsx new file mode 100644 index 000000000..7eba848bf --- /dev/null +++ b/packages/esm-patient-list-app/src/add-patient-to-patient-list-menu-item.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import AddPatientToPatientListMenuItem from './add-patient-to-patient-list-menu-item.component'; +import { showModal } from '@openmrs/esm-framework'; + +const mockedShowModal = showModal as jest.Mock; + +jest.mock('@openmrs/esm-framework'); + +const patientUuid = '6baa7963-68ea-497e-b258-6fb82382bd07'; + +describe('AddPatientToPatientListMenuItem', () => { + it('renders the button with the correct title', () => { + render(); + const button = screen.getByRole('menuitem'); + + expect(button).toBeInTheDocument(); + expect(button).toHaveTextContent('Add to list'); + expect(button).toHaveAttribute('title', 'Add to list'); + }); + + it('should open the modal on button click', async () => { + render(); + const button = screen.getByRole('menuitem'); + + fireEvent.click(button); + + expect(mockedShowModal).toHaveBeenCalledWith('add-patient-to-patient-list-modal', expect.any(Object)); + }); +}); From 2b33c58a1b44f1a3fdf667790cf9f4b7f4d8037d Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Sat, 26 Aug 2023 08:11:38 +0530 Subject: [PATCH 17/43] (test) O3-2224: Add tests for `text-person-attribute-field.component` (#766) --- .../text-person-attribute-field.component.tsx | 4 +- .../text-person-attribute-field.test.tsx | 88 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx index 0d1e1569f..ea9b153d0 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import styles from './../field.scss'; -import { Input } from '../../input/basic-input/input/input.component'; import { Field } from 'formik'; import { useTranslation } from 'react-i18next'; +import { Input } from '../../input/basic-input/input/input.component'; +import styles from './../field.scss'; import { PersonAttributeTypeResponse } from '../../patient-registration.types'; export interface TextPersonAttributeFieldProps { diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx new file mode 100644 index 000000000..2f65f0a68 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Form, Formik } from 'formik'; +import { TextPersonAttributeField } from './text-person-attribute-field.component'; + +describe('TextPersonAttributeField', () => { + const mockPersonAttributeType = { + format: 'java.lang.String', + display: 'Referred by', + uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0', + }; + + it('renders the input field with a label', () => { + render( + {}}> +
+ + +
, + ); + + expect(screen.getByRole('textbox', { name: /custom label \(optional\)/i })).toBeInTheDocument(); + }); + + it('renders the input field with the default label if label prop is not provided', () => { + render( + {}}> +
+ + +
, + ); + + expect(screen.getByRole('textbox', { name: /referred by \(optional\)/i })).toBeInTheDocument(); + }); + + it('validates the input with the provided validationRegex', async () => { + const user = userEvent.setup(); + const validationRegex = '^[A-Z]+$'; // Accepts only uppercase letters + + render( + {}}> +
+ + +
, + ); + + const textbox = screen.getByRole('textbox', { name: /referred by \(optional\)/i }); + expect(textbox).toBeInTheDocument(); + + // Valid input: "ABC" + await user.type(textbox, 'ABC'); + await user.tab(); + + expect(screen.queryByText(/invalid input/i)).not.toBeInTheDocument(); + await user.clear(textbox); + + // // Invalid input: "abc" (contains lowercase letters) + await user.type(textbox, 'abc'); + await user.tab(); + expect(screen.getByText(/invalid input/i)).toBeInTheDocument(); + }); + + it('renders the input field as required when required prop is true', () => { + render( + {}}> +
+ + +
, + ); + const textbox = screen.getByRole('textbox', { name: /referred by/i }); + + // Required attribute should be truthy on the input element + expect(textbox).toBeInTheDocument(); + expect(textbox).toBeRequired(); + }); +}); From 44c96c3bc4367f9861bd32a68357afc15ae1a974 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Sat, 26 Aug 2023 08:18:06 +0530 Subject: [PATCH 18/43] (test) O3-2222: Add tests for `person-attribute-field.component` component (#760) --- .../person-attribute-field.test.tsx | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx new file mode 100644 index 000000000..8dc81e01a --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { usePersonAttributeType } from './person-attributes.resource'; +import { PersonAttributeField } from './person-attribute-field.component'; +import { useConceptAnswers } from '../field.resource'; +import { Form, Formik } from 'formik'; +import { FieldDefinition } from '../../../config-schema'; + +jest.mock('./person-attributes.resource'); // Mock the usePersonAttributeType hook +jest.mock('../field.resource'); // Mock the useConceptAnswers hook + +jest.mock('formik', () => ({ + ...jest.requireActual('formik'), +})); + +const mockedUsePersonAttributeType = usePersonAttributeType as jest.Mock; +const mockedUseConceptAnswers = useConceptAnswers as jest.Mock; + +let fieldDefinition: FieldDefinition; + +describe('PersonAttributeField', () => { + let mockPersonAttributeType = { + format: 'java.lang.String', + display: 'Referred by', + uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0', + }; + + beforeEach(() => { + fieldDefinition = { + id: 'referredby', + name: 'Referred by', + type: 'person attribute', + uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0', + answerConceptSetUuid: '6682d17f-0777-45e4-a39b-93f77eb3531c', + validation: { + matches: '', + required: true, + }, + showHeading: true, + }; + mockedUsePersonAttributeType.mockReturnValue({ + data: mockPersonAttributeType, + isLoading: false, + error: null, + uuid: '14d4f066-15f5-102d-96e4-000c29c2a5d7d', + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('renders the text input field for String format', () => { + render( + {}}> +
+ + +
, + ); + + const input = screen.getByLabelText(/Referred by/i) as HTMLInputElement; + expect(screen.getByRole('heading')).toBeInTheDocument(); + expect(input).toBeInTheDocument(); + expect(input.type).toBe('text'); + }); + + it('should not show heading if showHeading is false', () => { + fieldDefinition = { + ...fieldDefinition, + showHeading: false, + }; + + render( + {}}> +
+ + +
, + ); + expect(screen.queryByRole('heading')).not.toBeInTheDocument(); + }); + + it('renders the coded attribute field for Concept format', () => { + mockedUsePersonAttributeType.mockReturnValue({ + data: { ...mockPersonAttributeType, format: 'org.openmrs.Concept' }, + isLoading: false, + error: null, + }); + + fieldDefinition = { + id: 'referredby', + ...fieldDefinition, + label: 'Referred by', + }; + + mockedUseConceptAnswers.mockReturnValueOnce({ + data: [ + { uuid: '1', display: 'Option 1' }, + { uuid: '2', display: 'Option 2' }, + ], + isLoading: false, + }); + + render( + {}}> +
+ + +
, + ); + + const input = screen.getByLabelText(/Referred by/i) as HTMLInputElement; + expect(input).toBeInTheDocument(); + expect(input.type).toBe('select-one'); + expect(screen.getByText('Option 1')).toBeInTheDocument(); + expect(screen.getByText('Option 2')).toBeInTheDocument(); + }); + + it('renders an error notification if attribute type has unknown format', () => { + mockedUsePersonAttributeType.mockReturnValue({ + data: { ...mockPersonAttributeType, format: 'unknown' }, + isLoading: false, + error: null, + }); + + render( + {}}> +
+ + +
, + ); + + expect(screen.getByText('Error')).toBeInTheDocument(); + expect(screen.getByText(/Patient attribute type has unknown format/i)).toBeInTheDocument(); + }); + it('renders an error notification if unable to fetch attribute type', () => { + mockedUsePersonAttributeType.mockReturnValue({ + data: null, + isLoading: false, + error: new Error('Failed to fetch attribute type'), + }); + + fieldDefinition = { + uuid: 'attribute-uuid', + label: 'Attribute', + showHeading: false, + }; + + render( + {}}> +
+ + +
, + ); + + expect(screen.getByText('Error')).toBeInTheDocument(); + expect(screen.getByText(/Unable to fetch person attribute type/i)).toBeInTheDocument(); + }); + + it('renders a skeleton if attribute type is loading', () => { + mockedUsePersonAttributeType.mockReturnValue({ + data: null, + isLoading: true, + error: null, + }); + + fieldDefinition = { + uuid: 'attribute-uuid', + label: 'Attribute', + showHeading: true, + }; + + render( + {}}> +
+ + +
, + ); + const input = screen.findByLabelText(/Reffered by/i); + expect(screen.getByText(/Attribute/i)).toBeInTheDocument(); + expect(input).not.toBeNull(); // checks that the input is not rendered when the attribute type is loading + }); +}); From ca7e91552d7038c17de74be2c4ecd915555640f4 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Sat, 26 Aug 2023 11:15:30 +0530 Subject: [PATCH 19/43] Add test for patient-table.component.tsx (#793) --- .../patient-table/patient-table.component.tsx | 18 ++- .../src/patient-table/patient-table.test.tsx | 138 ++++++++++++++++++ 2 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 packages/esm-patient-list-app/src/patient-table/patient-table.test.tsx diff --git a/packages/esm-patient-list-app/src/patient-table/patient-table.component.tsx b/packages/esm-patient-list-app/src/patient-table/patient-table.component.tsx index b164e4075..7cb439d3e 100644 --- a/packages/esm-patient-list-app/src/patient-table/patient-table.component.tsx +++ b/packages/esm-patient-list-app/src/patient-table/patient-table.component.tsx @@ -67,7 +67,7 @@ const PatientTable: React.FC = ({ () => patients.map((patient, index) => { const row = { - id: index, + id: String(index), }; columns.forEach((column) => { const value = column.getValue?.(patient) || patient[column.key]; @@ -88,7 +88,15 @@ const PatientTable: React.FC = ({ const otherSearchProps = useMemo(() => search.otherSearchProps || {}, [search]); if (isLoading) { - return ; + return ( + + ); } return ( @@ -101,7 +109,7 @@ const PatientTable: React.FC = ({ id="patient-list-search" placeholder={search.placeHolder} labelText="" - size={isDesktop(layout) ? 'sm' : 'xl'} + size={isDesktop(layout) ? 'sm' : 'lg'} className={styles.searchOverrides} onChange={(evnt) => handleSearch(evnt.target.value)} defaultValue={search.currentSearchTerm} @@ -154,8 +162,8 @@ const PatientTable: React.FC = ({ className={styles.paginationOverride} pagesUnknown={pagination?.pagesUnknown} isLastPage={pagination.lastPage} - backwardText="" - forwardText="" + backwardText="Next Page" + forwardText="Previous Page" /> )}
diff --git a/packages/esm-patient-list-app/src/patient-table/patient-table.test.tsx b/packages/esm-patient-list-app/src/patient-table/patient-table.test.tsx new file mode 100644 index 000000000..4835a6ea4 --- /dev/null +++ b/packages/esm-patient-list-app/src/patient-table/patient-table.test.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import PatientTable from './patient-table.component'; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + isDesktop: jest.fn(() => true), +})); + +describe('PatientTable Component', () => { + const patients = [ + { + id: '123abced', + firstName: 'John', + lastName: 'Doe', + age: 30, + }, + { + id: '123abcedfg', + firstName: 'Jane', + lastName: 'Smith', + age: 25, + }, + ]; + + const columns = [ + { + key: 'firstName', + header: 'First Name', + }, + { + key: 'lastName', + header: 'Last Name', + link: { + getUrl: (patient) => `/patient/${patient.id}`, + }, + }, + { + key: 'age', + header: 'Age', + getValue: (patient) => `${patient.age} years`, + }, + ]; + + const mockedOnSearch = jest.fn(); + + const search = { + onSearch: mockedOnSearch, + placeHolder: 'Search Patients', + }; + + const mockedOnChange = jest.fn(); + + let pagination = { + usePagination: true, + currentPage: 1, + onChange: mockedOnChange, + pageSize: 10, + totalItems: 100, + pagesUnknown: false, + }; + + it('renders table with patient data', () => { + render( + , + ); + expect(screen.getByTestId('patientsTable')).toBeInTheDocument(); + }); + + it('renders loading skeleton when loading', () => { + render( + , + ); + + expect(screen.getByTestId('data-table-skeleton')).toBeInTheDocument(); + }); + + it('performs search when typing in the search input', async () => { + render( + , + ); + + const searchInput = screen.getByPlaceholderText('Search Patients'); + const searchText = 'John Doe'; + + fireEvent.change(searchInput, { target: { value: searchText } }); + + expect(searchInput).toHaveValue(searchText); + await waitFor(() => expect(mockedOnSearch).toHaveBeenCalledWith(searchText)); + }); + + //need to fix this test + xit('calls onChange when clicking pagination buttons', async () => { + render( + , + ); + + // Click on the next page button + fireEvent.click(screen.getByRole('button', { name: /Next page/i })); + + // Expect the onChange function to be called with the new page number + await waitFor(() => expect(mockedOnChange).toHaveBeenCalledWith({ page: 2, pageSize: 10 })); + // + expect(pagination.currentPage).toBe(2); + }); +}); From 6ae79c173ffaac6a889949298a4ea361bfbdb4f2 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Sat, 26 Aug 2023 13:45:24 +0530 Subject: [PATCH 20/43] (test) O3-2329: Add tests for the `esm-outpatient-app` (#797) * add tests for home.component.tsx * add test for overlay component * add test for root.component.tsx * add test for active-visits-tab.component.tsx * add test for add-provider-queue-room.component.tsx * add test for clear-queue-entries-dialog.component.tsx * add test for current-visit-summary.component.tsx * add test for base-visit-type.test.tsx * add test for visit-form-queue-fields.component.tsx * add test for queue-linelist-filter.component.tsx * add test for queue-linelist.component.tsx * add test for schedules-appointments-table.component.tsx * queue-service-form.component.tsx * add test for queue-service-form.component.tsx * remove-queue-entry.test.tsx * add test side-menu.component.tsx * add test for nav-group.component.tsx * transition-queue-entry-dialog --- .../active-visits-tab.component.tsx | 2 +- .../active-visits/active-visits-tab.test.tsx | 19 ++++ .../add-provider-queue-room.test.tsx | 98 +++++++++++++++++++ .../clear-queue-entries-dialog.test.tsx | 42 ++++++++ .../current-visit-summary.test.tsx | 47 +++++++++ packages/esm-outpatient-app/src/home.test.tsx | 58 +++++++++++ .../esm-outpatient-app/src/overlay.test.tsx | 49 ++++++++++ .../patient-queue-header.component.tsx | 2 +- .../clinic-metrics.component.tsx | 2 +- .../visit-form-queue-fields.test.tsx | 67 +++++++++++++ .../visit-form/base-visit-type.test.tsx | 41 ++++++++ .../queue-linelist-filter.test.tsx | 76 ++++++++++++++ .../queue-linelist.test.tsx | 22 +++++ .../scheduled-appointments-table.test.tsx | 42 ++++++++ .../src/queue-rooms/queue-room-form.test.tsx | 65 ++++++++++++ .../queue-service-form.component.tsx | 2 +- .../queue-service-form.test.tsx | 56 +++++++++++ .../remove-queue-entry.test.tsx | 44 +++++++++ packages/esm-outpatient-app/src/root.test.tsx | 28 ++++++ .../side-menu/nav-group/nav-group.test.tsx | 32 ++++++ .../src/side-menu/side-menu.test.tsx | 17 ++++ .../transition-queue-entry-dialog.test.tsx | 95 ++++++++++++++++++ 22 files changed, 902 insertions(+), 4 deletions(-) create mode 100644 packages/esm-outpatient-app/src/active-visits/active-visits-tab.test.tsx create mode 100644 packages/esm-outpatient-app/src/add-provider-queue-room/add-provider-queue-room.test.tsx create mode 100644 packages/esm-outpatient-app/src/clear-queue-entries-dialog/clear-queue-entries-dialog.test.tsx create mode 100644 packages/esm-outpatient-app/src/current-visit/current-visit-summary.test.tsx create mode 100644 packages/esm-outpatient-app/src/home.test.tsx create mode 100644 packages/esm-outpatient-app/src/overlay.test.tsx create mode 100644 packages/esm-outpatient-app/src/patient-search/visit-form-queue-fields/visit-form-queue-fields.test.tsx create mode 100644 packages/esm-outpatient-app/src/patient-search/visit-form/base-visit-type.test.tsx create mode 100644 packages/esm-outpatient-app/src/queue-patient-linelists/queue-linelist-filter.test.tsx create mode 100644 packages/esm-outpatient-app/src/queue-patient-linelists/queue-linelist.test.tsx create mode 100644 packages/esm-outpatient-app/src/queue-patient-linelists/scheduled-appointments-table.test.tsx create mode 100644 packages/esm-outpatient-app/src/queue-rooms/queue-room-form.test.tsx create mode 100644 packages/esm-outpatient-app/src/queue-services/queue-service-form.test.tsx create mode 100644 packages/esm-outpatient-app/src/remove-queue-entry-dialog/remove-queue-entry.test.tsx create mode 100644 packages/esm-outpatient-app/src/root.test.tsx create mode 100644 packages/esm-outpatient-app/src/side-menu/nav-group/nav-group.test.tsx create mode 100644 packages/esm-outpatient-app/src/side-menu/side-menu.test.tsx create mode 100644 packages/esm-outpatient-app/src/transition-queue-entry/transition-queue-entry-dialog.test.tsx diff --git a/packages/esm-outpatient-app/src/active-visits/active-visits-tab.component.tsx b/packages/esm-outpatient-app/src/active-visits/active-visits-tab.component.tsx index 4970d39f0..16acfa678 100644 --- a/packages/esm-outpatient-app/src/active-visits/active-visits-tab.component.tsx +++ b/packages/esm-outpatient-app/src/active-visits/active-visits-tab.component.tsx @@ -18,7 +18,7 @@ function ActiveVisitsTabs() { const [overlayHeader, setOverlayTitle] = useState(''); return ( -
+
({ + ...jest.requireActual('@openmrs/esm-framework'), + useConfig: jest.fn(() => ({ + concepts: { + visitQueueNumberAttributeUuid: 'c61ce16f-272a-41e7-9924-4c555d0932c5', + }, + })), +})); + +describe('ActiveVisitsTabs', () => { + it('renders the component', () => { + const { container } = render(); + expect(container).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-outpatient-app/src/add-provider-queue-room/add-provider-queue-room.test.tsx b/packages/esm-outpatient-app/src/add-provider-queue-room/add-provider-queue-room.test.tsx new file mode 100644 index 000000000..38856df9d --- /dev/null +++ b/packages/esm-outpatient-app/src/add-provider-queue-room/add-provider-queue-room.test.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import AddProviderQueueRoom from './add-provider-queue-room.component'; +import { showToast } from '@openmrs/esm-framework'; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + useCurrentProvider: jest.fn(() => ({ + currentProvider: { uuid: 'provider-uuid-1' }, + })), + showToast: jest.fn(), + showNotification: jest.fn(), +})); +jest.mock('./add-provider-queue-room.resource', () => ({ + useProvidersQueueRoom: jest.fn(() => ({ + providerRoom: [ + { + queueRoom: { uuid: 'c187d78b-5c54-49bf-a0f8-b7fb6034d36d' }, + uuid: '6b3e233d-2b44-40ca-b0c8-c5a57a8c51b6', + }, + ], + isLoading: false, + mutate: jest.fn(), + })), + updateProviderToQueueRoom: jest.fn().mockResolvedValue({ status: 200 }), + addProviderToQueueRoom: jest.fn(), + useQueueRooms: jest.fn(() => ({ + rooms: [ + { uuid: '6b3e233d-2b44-40ca-b0c8-c5a57a8c51b6', display: 'Room 1' }, + { uuid: 'e7786ac0-ab62-11ec-b909-0242ac120002', display: 'Room 2' }, + ], + })), +})); + +jest.mock('../active-visits/active-visits-table.resource', () => ({ + useServices: jest.fn(() => ({ + services: [ + { uuid: 'e7786ac0-ab62-11ec-b909-0242ac120002', display: 'Service 1' }, + { uuid: 'e7786ac0-ab62-11ec-b909-0242ac120032', display: 'Service 2' }, + ], + })), +})); + +jest.mock('../patient-search/hooks/useQueueLocations', () => ({ + useQueueLocations: jest.fn(() => ({ + queueLocations: [ + { id: '1GHI12', name: 'Location 1' }, + { id: '1GHI45', name: 'Location 2' }, + ], + })), +})); + +const providerUuid = 'cc75ad73-c24b-499c-8db9-a7ef4fc0b36d'; + +describe('AddProviderQueueRoom', () => { + it('renders the form fields', () => { + render(); + + expect(screen.getByText('Select a room')).toBeInTheDocument(); + expect(screen.getByLabelText('Retain location')).toBeInTheDocument(); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByText('Save')).toBeInTheDocument(); + }); + + it('updates queue room selection', () => { + render(); + + const selectQueueRoom: HTMLInputElement = screen.getByRole('combobox'); + fireEvent.change(selectQueueRoom, { target: { value: 'room-uuid-1' } }); + + expect(selectQueueRoom.value).toBe('6b3e233d-2b44-40ca-b0c8-c5a57a8c51b6'); + }); + + it('should update the retain location checkbox', () => { + render(); + + const retainLocationCheckbox: HTMLInputElement = screen.getByRole('checkbox'); + fireEvent.click(retainLocationCheckbox); + + expect(retainLocationCheckbox.checked).toBe(true); + }); + + it('should submit the form and add provider to queue room when all fields are filled', async () => { + const mockCloseModal = jest.fn(); + render(); + + const queueRoomSelect = screen.getByRole('combobox'); + const submitButton = screen.getByText('Save'); + + fireEvent.change(queueRoomSelect, { target: { value: '6b3e233d-2b44-40ca-b0c8-c5a57a8c51b6' } }); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(mockCloseModal).toHaveBeenCalled(); + expect(showToast).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/esm-outpatient-app/src/clear-queue-entries-dialog/clear-queue-entries-dialog.test.tsx b/packages/esm-outpatient-app/src/clear-queue-entries-dialog/clear-queue-entries-dialog.test.tsx new file mode 100644 index 000000000..0282c5512 --- /dev/null +++ b/packages/esm-outpatient-app/src/clear-queue-entries-dialog/clear-queue-entries-dialog.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import ClearQueueEntriesDialog from './clear-queue-entries-dialog.component'; +import { batchClearQueueEntries } from './clear-queue-entries-dialog.resource'; + +const mockBatchClearQueueEntries = batchClearQueueEntries as jest.Mock; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + showToast: jest.fn(), +})); + +jest.mock('./clear-queue-entries-dialog.resource'); + +jest.mock('../active-visits/active-visits-table.resource', () => ({ + useVisitQueueEntries: () => ({ + mutate: jest.fn(), + }), +})); + +describe('ClearQueueEntriesDialog Component', () => { + const visitQueueEntriesMock = []; + + it('renders the component with warning message', () => { + render( {}} />); + + expect(screen.getByRole('heading', { name: 'Service queue' })).toBeInTheDocument(); + expect(screen.getByText('Clear all queue entries?')).toBeInTheDocument(); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByText('Clear queue')).toBeInTheDocument(); + }); + + it('should close modal when clicked on cancel', async () => { + const closeModalMock = jest.fn(); + mockBatchClearQueueEntries.mockImplementationOnce(() => Promise.resolve()); + render(); + + fireEvent.click(screen.getByText('Cancel')); + + expect(closeModalMock).toHaveBeenCalled(); + }); +}); diff --git a/packages/esm-outpatient-app/src/current-visit/current-visit-summary.test.tsx b/packages/esm-outpatient-app/src/current-visit/current-visit-summary.test.tsx new file mode 100644 index 000000000..2d5725998 --- /dev/null +++ b/packages/esm-outpatient-app/src/current-visit/current-visit-summary.test.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import CurrentVisit from './current-visit-summary.component'; +import { useVisit } from './current-visit.resource'; +import { mockPastVisit } from '../../__mocks__/visits.mock'; + +const useVisitMock = useVisit as jest.Mock; + +jest.mock('./current-visit.resource', () => ({ + useVisit: jest.fn(() => ({ + visit: { + visitType: { display: 'Visit Type' }, + encounters: [], + }, + isError: false, + isLoading: false, + })), +})); + +const patientUuid = mockPastVisit.data.results[0].patient.uuid; +const visitUuid = mockPastVisit.data.results[0].uuid; + +describe('CurrentVisit', () => { + it('renders visit details correctly', async () => { + render(); + + await waitFor(() => { + expect(screen.queryByRole('progressbar')).toBeNull(); + expect(screen.getByText('Visit Type')).toBeInTheDocument(); + expect(screen.getByText('Scheduled for today')).toBeInTheDocument(); + expect(screen.getByText('On time')).toBeInTheDocument(); + }); + }); + it('should render skeleton when loading', async () => { + useVisitMock.mockImplementationOnce(() => ({ + visit: undefined, + isError: false, + isLoading: true, + })); + + render(); + + await waitFor(() => { + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/esm-outpatient-app/src/home.test.tsx b/packages/esm-outpatient-app/src/home.test.tsx new file mode 100644 index 000000000..d6c676b63 --- /dev/null +++ b/packages/esm-outpatient-app/src/home.test.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import Home from './home.component'; +import { useConfig } from '@openmrs/esm-framework'; + +const mockedUseConfig = useConfig as jest.Mock; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + useConfig: jest.fn(() => ({ + showQueueTableTab: true, + concepts: { + visitQueueNumberAttributeUuid: 'c61ce16f-272a-41e7-9924-4c555d0932c5', + }, + })), +})); + +describe('Home Component', () => { + it('renders PatientQueueHeader, ClinicMetrics, and ActiveVisitsTabs when activeTicketScreen is not "screen"', () => { + // Mock window.location.pathname + const originalLocation = window.location; + delete window.location; + window.location = { pathname: '/some-path' }; + + render(); + + // Assert that the expected components are rendered + expect(screen.getByTestId('patient-queue-header')).toBeInTheDocument(); + expect(screen.getByTestId('clinic-metrics')).toBeInTheDocument(); + expect(screen.getByTestId('active-visits-tabs')).toBeInTheDocument(); + + // Clean up the mock + window.location = originalLocation; + }); + + it('renders QueueScreen when activeTicketScreen is "screen"', () => { + const originalLocation = window.location; + delete window.location; + window.location = { pathname: '/some-path/screen' }; + + render(); + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + + window.location = originalLocation; + }); + + it('renders ActiveVisitsTable when showQueueTableTab is false', () => { + mockedUseConfig.mockImplementationOnce(() => ({ + showQueueTableTab: false, + concepts: { + visitQueueNumberAttributeUuid: 'c61ce16f-272a-41e7-9924-4c555d0932c5', + }, + })); + render(); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-outpatient-app/src/overlay.test.tsx b/packages/esm-outpatient-app/src/overlay.test.tsx new file mode 100644 index 000000000..ea35b6440 --- /dev/null +++ b/packages/esm-outpatient-app/src/overlay.test.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import Overlay from './overlay.component'; +import { useLayoutType, isDesktop } from '@openmrs/esm-framework'; + +const mockUseLayoutType = useLayoutType as jest.Mock; +const mockIsDesktop = isDesktop as jest.Mock; + +jest.mock('@openmrs/esm-framework'); + +const headerText = 'Test Header'; + +describe('Overlay Component', () => { + it('renders desktop layout with close button', () => { + const closePanelMock = jest.fn(); + + mockIsDesktop.mockImplementation(() => true); + + render(); + + const headerContent = screen.getByText(headerText); + const closeButton = screen.getByLabelText('Close overlay'); + + expect(headerContent).toBeInTheDocument(); + expect(closeButton).toBeInTheDocument(); + + fireEvent.click(closeButton); + expect(closePanelMock).toHaveBeenCalled(); + }); + + it('renders tablet layout with back arrow button and the children', () => { + const closePanelMock = jest.fn(); + mockIsDesktop.mockImplementation(() => false); + mockUseLayoutType.mockImplementation(() => 'tablet'); + + render( + +
Something
+
, + ); + + const headerContent = screen.getByText(headerText); + const backButton = screen.getByText('Close overlay'); + + expect(headerContent).toBeInTheDocument(); + expect(backButton).toBeInTheDocument(); + expect(screen.getByText('Something')).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-outpatient-app/src/patient-queue-header/patient-queue-header.component.tsx b/packages/esm-outpatient-app/src/patient-queue-header/patient-queue-header.component.tsx index aac0e27f9..06576a3b3 100644 --- a/packages/esm-outpatient-app/src/patient-queue-header/patient-queue-header.component.tsx +++ b/packages/esm-outpatient-app/src/patient-queue-header/patient-queue-header.component.tsx @@ -28,7 +28,7 @@ const PatientQueueHeader: React.FC<{ title?: string }> = ({ title }) => { return ( <> -
+
diff --git a/packages/esm-outpatient-app/src/patient-queue-metrics/clinic-metrics.component.tsx b/packages/esm-outpatient-app/src/patient-queue-metrics/clinic-metrics.component.tsx index e67b2c17e..262a1901a 100644 --- a/packages/esm-outpatient-app/src/patient-queue-metrics/clinic-metrics.component.tsx +++ b/packages/esm-outpatient-app/src/patient-queue-metrics/clinic-metrics.component.tsx @@ -56,7 +56,7 @@ function ClinicMetrics() { return ( <> -
+
({ + ...jest.requireActual('@openmrs/esm-framework'), + useLayoutType: () => 'desktop', + showNotification: jest.fn(), + showToast: jest.fn(), + useConfig: jest.fn(() => ({ + concepts: { + visitQueueNumberAttributeUuid: 'c61ce16f-272a-41e7-9924-4c555d0932c5', + defaultStatusConceptUuid: 'c61ce16f-272a-41e7-9924-4c555d0932c5', + }, + })), +})); +jest.mock('../hooks/useQueueLocations', () => ({ + useQueueLocations: jest.fn(() => ({ + queueLocations: [{ id: '1', name: 'Location 1' }], + })), +})); + +jest.mock('../../active-visits/active-visits-table.resource', () => ({ + usePriority: jest.fn(() => ({ + priorities: [{ uuid: '197852c7-5fd4-4b33-89cc-7bae6848c65a', display: 'High' }], + })), + useStatus: jest.fn(() => ({ + statuses: [{ uuid: '176052c7-5fd4-4b33-89cc-7bae6848c65a', display: 'In Progress' }], + })), +})); + +jest.mock('../../patient-queue-metrics/queue-metrics.resource', () => ({ + useServices: jest.fn((selectedQueueLocation) => ({ + allServices: [{ uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', name: 'Service 1' }], + isLoading: false, + })), +})); + +describe('StartVisitQueueFields', () => { + it('renders the form fields', () => { + const { getByLabelText, getByText } = render(); + + expect(getByLabelText('Select a queue location')).toBeInTheDocument(); + expect(getByLabelText('Select a service')).toBeInTheDocument(); + expect(getByLabelText('Select a status')).toBeInTheDocument(); + expect(getByText('High')).toBeInTheDocument(); + expect(getByLabelText('Sort weight')).toBeInTheDocument(); + }); + + it('updates the selected queue location', () => { + const { getByLabelText } = render(); + + const selectQueueLocation = getByLabelText('Select a queue location') as HTMLInputElement; + fireEvent.change(selectQueueLocation, { target: { value: '1' } }); + + expect(selectQueueLocation.value).toBe('1'); + }); + + it('updates the selected service', () => { + const { getByLabelText } = render(); + + const selectService = getByLabelText('Select a service') as HTMLInputElement; + fireEvent.change(selectService, { target: { value: 'service-1' } }); + + expect(selectService.value).toBe('e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90'); + }); +}); diff --git a/packages/esm-outpatient-app/src/patient-search/visit-form/base-visit-type.test.tsx b/packages/esm-outpatient-app/src/patient-search/visit-form/base-visit-type.test.tsx new file mode 100644 index 000000000..895e46d6f --- /dev/null +++ b/packages/esm-outpatient-app/src/patient-search/visit-form/base-visit-type.test.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import BaseVisitType from './base-visit-type.component'; +import { mockVisitTypes } from '../../../__mocks__/visits.mock'; + +jest.mock('@openmrs/esm-framework', () => ({ + useLayoutType: () => 'desktop', + usePagination: jest.fn(() => ({ results: mockVisitTypes, currentPage: 1, goTo: jest.fn() })), +})); + +describe('BaseVisitType', () => { + it('renders visit types correctly', () => { + render( {}} visitTypes={mockVisitTypes} />); + + const searchInput = screen.getByRole('searchbox'); + expect(searchInput).toBeInTheDocument(); + + mockVisitTypes.forEach((visitType) => { + const radioButton = screen.getByLabelText(visitType.display); + expect(radioButton).toBeInTheDocument(); + }); + }); + + it('handles search input correctly', () => { + render( {}} visitTypes={mockVisitTypes} />); + + const searchInput: HTMLInputElement = screen.getByRole('searchbox'); + fireEvent.change(searchInput, { target: { value: 'Visit Type 1' } }); + + expect(searchInput.value).toBe('Visit Type 1'); + }); + + it('calls onChange when a visit type is selected', () => { + const mockOnChange = jest.fn(); + render(); + + const radioButton: HTMLInputElement = screen.getByLabelText(mockVisitTypes[0].display); + fireEvent.click(radioButton); + expect(radioButton.checked).toBe(true); + }); +}); diff --git a/packages/esm-outpatient-app/src/queue-patient-linelists/queue-linelist-filter.test.tsx b/packages/esm-outpatient-app/src/queue-patient-linelists/queue-linelist-filter.test.tsx new file mode 100644 index 000000000..2dacd0fdc --- /dev/null +++ b/packages/esm-outpatient-app/src/queue-patient-linelists/queue-linelist-filter.test.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import QueueLinelistFilter from './queue-linelist-filter.component'; +import { mockVisitTypes } from '../../__mocks__/visits.mock'; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + useLayoutType: jest.fn(() => 'tablet'), + useVisitTypes: jest.fn(() => mockVisitTypes), + toOpemrsIsoString: jest.fn(), +})); +// Additional mock functions if needed + +describe('QueueLinelistFilter', () => { + it('renders the form with filter elements', () => { + render(); + + expect(screen.getByText('Gender')).toBeInTheDocument(); + expect(screen.getByLabelText('Age')).toBeInTheDocument(); + expect(screen.getByLabelText('Between')).toBeInTheDocument(); + expect(screen.getByLabelText('And')).toBeInTheDocument(); + expect(screen.getByLabelText('Date')).toBeInTheDocument(); + expect(screen.getByText("Use today's date")).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Select visit type/i })).toBeInTheDocument(); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByText('Apply filters')).toBeInTheDocument(); + }); + + it('calls closePanel function when cancel button is clicked', () => { + const closePanelMock = jest.fn(); + render(); + + const cancelButton = screen.getByText('Cancel'); + fireEvent.click(cancelButton); + + expect(closePanelMock).toHaveBeenCalledTimes(1); + }); + + it('updates gender state when a radio button is selected', () => { + render(); + + const maleRadioButton = screen.getByLabelText('Male'); + fireEvent.click(maleRadioButton); + + expect(maleRadioButton).toBeChecked(); + }); + + it('updates startAge state when a number is entered', () => { + render(); + + const startAgeInput = screen.getByLabelText('Between'); + fireEvent.change(startAgeInput, { target: { value: '10' } }); + + expect(startAgeInput).toHaveValue(10); + }); + + it('updates returnDate state when date input changes', () => { + render(); + + const returnDateInput = screen.getByLabelText('Date'); + fireEvent.change(returnDateInput, { target: { value: '2023-08-20' } }); + + expect(returnDateInput).toHaveValue('2023-08-20'); + }); + + it('should open the visit type dropdown and close after selection', () => { + render(); + + const visitTypeDropdown = screen.getByRole('button', { name: /Select visit type/i }); + fireEvent.click(visitTypeDropdown); + const type1Option = screen.getByText('Outpatient Visit'); + fireEvent.click(type1Option); + + expect(visitTypeDropdown).toHaveTextContent('Open menu'); + }); +}); diff --git a/packages/esm-outpatient-app/src/queue-patient-linelists/queue-linelist.test.tsx b/packages/esm-outpatient-app/src/queue-patient-linelists/queue-linelist.test.tsx new file mode 100644 index 000000000..6476bb8b0 --- /dev/null +++ b/packages/esm-outpatient-app/src/queue-patient-linelists/queue-linelist.test.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import QueueLinelist from './queue-linelist.component'; + +describe('QueueLinelist', () => { + it('renders with filter content initially', () => { + render(); + + const filterContent = screen.getByText('Filters'); + expect(filterContent).toBeInTheDocument(); + }); + + it('toggles between filter content and null', () => { + render(); + + const closeButton = screen.getByText('Close overlay'); + fireEvent.click(closeButton); + + const filterContent = screen.queryByTestId('filter-content'); + expect(filterContent).not.toBeInTheDocument(); + }); +}); diff --git a/packages/esm-outpatient-app/src/queue-patient-linelists/scheduled-appointments-table.test.tsx b/packages/esm-outpatient-app/src/queue-patient-linelists/scheduled-appointments-table.test.tsx new file mode 100644 index 000000000..e179d6262 --- /dev/null +++ b/packages/esm-outpatient-app/src/queue-patient-linelists/scheduled-appointments-table.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import AppointmentsTable from './scheduled-appointments-table.component'; +import { mockAppointmentsData } from '../../__mocks__/appointments-data.mock'; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + useConfig: () => ({ + appointmentStatuses: ['All', 'Scheduled', 'Completed'], + }), + usePagination: () => ({ + goTo: jest.fn(), + results: mockAppointmentsData.data, + currentPage: 1, + }), +})); + +jest.mock('./queue-linelist.resource', () => ({ + useAppointments: () => ({ + appointmentQueueEntries: mockAppointmentsData.data, + isLoading: false, + }), +})); + +describe('AppointmentsTable', () => { + it('renders appointments when loading is complete', () => { + render(); + + const appointmentName = screen.getByText('Hungai Kevin'); + expect(appointmentName).toBeInTheDocument(); + }); + + it('filters appointments based on status selection', () => { + render(); + + const statusDropdown = screen.getAllByLabelText('Status:'); + fireEvent.change(statusDropdown[0], { target: { value: 'Completed' } }); + + const filteredAppointmentName = screen.getByText('Hungai Kevin'); + expect(filteredAppointmentName).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-outpatient-app/src/queue-rooms/queue-room-form.test.tsx b/packages/esm-outpatient-app/src/queue-rooms/queue-room-form.test.tsx new file mode 100644 index 000000000..937d9ea5f --- /dev/null +++ b/packages/esm-outpatient-app/src/queue-rooms/queue-room-form.test.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import QueueRoomForm from './queue-room-form.component'; +import { mockServices } from '../../__mocks__/active-visits.mock'; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + useLayoutType: jest.fn(() => 'tablet'), + useServices: jest.fn(() => ({ services: mockServices })), + useQueueLocations: jest.fn(() => ({ + queueLocations: { uuid: 'e7786d9a-ab62-11ec-b909-0242ac120002', display: 'Location Test' }, + })), + showToast: jest.fn(), +})); + +jest.mock('./queue-room.resource', () => ({ + saveQueueRoom: jest.fn(() => Promise.resolve({ status: 201 })), +})); + +describe('QueueRoomForm', () => { + it('renders the form with queue room elements', () => { + render(); + + expect(screen.getByLabelText('Queue room name')).toBeInTheDocument(); + expect(screen.getByLabelText('Select a service')).toBeInTheDocument(); + expect(screen.getByLabelText('Select a queue location')).toBeInTheDocument(); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByText('Save')).toBeInTheDocument(); + }); + + it('displays error notification if queue room name is missing on submission', () => { + render(); + + fireEvent.click(screen.getByText('Save')); + + expect(screen.getByText('Missing queue room name')).toBeInTheDocument(); + }); + + it('displays error notification if queue room service is missing on submission', () => { + render(); + + fireEvent.change(screen.getByLabelText('Queue room name'), { target: { value: 'Room 123' } }); + fireEvent.click(screen.getByText('Save')); + + expect(screen.getByText('Missing queue room service')).toBeInTheDocument(); + }); + + it('calls closePanel when Cancel button is clicked', () => { + const closePanelMock = jest.fn(); + render(); + + fireEvent.click(screen.getByText('Cancel')); + + expect(closePanelMock).toHaveBeenCalledTimes(1); + }); + + it('updates queue room name state when a value is entered', () => { + render(); + + const queueRoomNameInput = screen.getByLabelText('Queue room name'); + fireEvent.change(queueRoomNameInput, { target: { value: 'Room 123' } }); + + expect(queueRoomNameInput).toHaveValue('Room 123'); + }); +}); diff --git a/packages/esm-outpatient-app/src/queue-services/queue-service-form.component.tsx b/packages/esm-outpatient-app/src/queue-services/queue-service-form.component.tsx index 625824a72..30b7cf9c6 100644 --- a/packages/esm-outpatient-app/src/queue-services/queue-service-form.component.tsx +++ b/packages/esm-outpatient-app/src/queue-services/queue-service-form.component.tsx @@ -111,7 +111,7 @@ const QueueServiceForm: React.FC = ({ toggleSearchType, c @@ -224,3 +152,79 @@ const RelationshipView: React.FC = ({ /> ); }; + +export const RelationshipsSection = () => { + const { relationshipTypes } = useContext(ResourcesContext); + const [displayRelationshipTypes, setDisplayRelationshipTypes] = useState([]); + const { t } = useTranslation(); + + useEffect(() => { + if (relationshipTypes) { + const tmp: RelationshipType[] = []; + relationshipTypes.results.forEach((type) => { + const aIsToB = { + display: type.aIsToB, + uuid: type.uuid, + direction: 'aIsToB', + }; + const bIsToA = { + display: type.bIsToA, + uuid: type.uuid, + direction: 'bIsToA', + }; + aIsToB.display === bIsToA.display ? tmp.push(aIsToB) : tmp.push(aIsToB, bIsToA); + }); + setDisplayRelationshipTypes(tmp); + } + }, [relationshipTypes]); + + if (!relationshipTypes) { + return ( +
+ +
+ ); + } + + return ( +
+ + {({ + push, + remove, + form: { + values: { relationships }, + }, + }) => ( +
+ {relationships && relationships.length > 0 + ? relationships.map((relationship: RelationshipValue, index) => ( +
+ +
+ )) + : null} +
+ +
+
+ )} +
+
+ ); +}; diff --git a/packages/esm-patient-registration-app/src/patient-registration/section/patient-relationships/relationships-section.test.tsx b/packages/esm-patient-registration-app/src/patient-registration/section/patient-relationships/relationships-section.test.tsx new file mode 100644 index 000000000..825f30de5 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-registration/section/patient-relationships/relationships-section.test.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Form, Formik } from 'formik'; +import { render, screen } from '@testing-library/react'; +import { PatientRegistrationContext } from '../../patient-registration-context'; +import { Resources, ResourcesContext } from '../../../offline.resources'; +import { RelationshipsSection } from './relationships-section.component'; + +jest.mock('../../patient-registration.resource', () => ({ + fetchPerson: jest.fn().mockResolvedValue({ + data: { + results: [ + { uuid: '42ae5ce0-d64b-11ea-9064-5adc43bbdd24', display: 'Person 1' }, + { uuid: '691eed12-c0f1-11e2-94be-8c13b969e334', display: 'Person 2' }, + ], + }, + }), +})); + +let mockResourcesContextValue = { + addressTemplate: [], + currentSession: { + authenticated: true, + sessionId: 'JSESSION', + currentProvider: { uuid: '45ce6c2e-dd5a-11e6-9d9c-0242ac150002', identifier: 'PRO-123' }, + }, + identifierTypes: [], + relationshipTypes: null, +} as Resources; + +describe('RelationshipsSection', () => { + it('renders a loader when relationshipTypes are not available', () => { + render( + + +
+ + +
+
, + ); + + expect(screen.getByLabelText(/loading relationships section/i)).toBeInTheDocument(); + expect(screen.getByRole(/progressbar/i)).toBeInTheDocument(); + expect(screen.queryByText(/add relationship/i)).not.toBeInTheDocument(); + }); + + it('renders relationships when relationshipTypes are available', () => { + const relationshipTypes = { + results: [ + { aIsToB: 'Mother', bIsToA: 'Child', uuid: '42ae5ce0-d64b-11ea-9064-5adc43bbdd34' }, + { aIsToB: 'Father', bIsToA: 'Child', uuid: '52ae5ce0-d64b-11ea-9064-5adc43bbdd24' }, + ], + }; + mockResourcesContextValue = { + ...mockResourcesContextValue, + relationshipTypes: relationshipTypes, + }; + + render( + + +
+ + + +
+
+
, + ); + + expect(screen.getByLabelText(/relationships section/i)).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /relationship/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /add relationship/i })).toBeInTheDocument(); + expect(screen.getByRole('searchbox', { name: /full name/i })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: /mother/i })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: /father/i })).toBeInTheDocument(); + expect(screen.getAllByRole('option', { name: /child/i }).length).toEqual(2); + }); +}); From e11242c022778694704ca7bd7c056b96571f5ee6 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Sat, 26 Aug 2023 14:38:41 +0530 Subject: [PATCH 24/43] (test) O3-2289: Add test for `active-visits.component` (#770) --- .../active-visits.test.tsx | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 packages/esm-active-visits-app/src/active-visits-widget/active-visits.test.tsx diff --git a/packages/esm-active-visits-app/src/active-visits-widget/active-visits.test.tsx b/packages/esm-active-visits-app/src/active-visits-widget/active-visits.test.tsx new file mode 100644 index 000000000..51f20ed7c --- /dev/null +++ b/packages/esm-active-visits-app/src/active-visits-widget/active-visits.test.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import ActiveVisitsTable from './active-visits.component'; +import { useConfig, usePagination } from '@openmrs/esm-framework'; +import { useActiveVisits } from './active-visits.resource'; + +const mockedUsePagination = usePagination as jest.Mock; +const mockActiveVisits = useActiveVisits as jest.Mock; + +const mockActiveVisitsData = [{ id: '1', name: 'John Doe', visitType: 'Checkup', patientUuid: 'uuid1' }]; +jest.mock('./active-visits.resource', () => ({ + ...jest.requireActual('./active-visits.resource'), + useActiveVisits: jest.fn(() => ({ + activeVisits: mockActiveVisitsData, + isLoading: false, + isValidating: false, + error: null, + })), +})); + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + useConfig: jest.fn(() => ({ activeVisits: { pageSizes: [10, 20, 30, 40, 50], pageSize: 10 } })), + usePagination: jest.fn().mockImplementation((data) => ({ + currentPage: 1, + goTo: () => {}, + results: data, + paginated: false, + })), +})); + +describe('ActiveVisitsTable', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders data table with active visits', () => { + render(); + + expect(screen.getByText('Visit Time')).toBeInTheDocument(); + expect(screen.getByText('ID Number')).toBeInTheDocument(); + const expectedColumnHeaders = [/Visit Time/, /ID Number/, /Name/, /Gender/, /Age/, /Visit Type/]; + expectedColumnHeaders.forEach((header) => { + expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument(); + }); + + const patientNameLink = screen.getByText('John Doe'); + expect(patientNameLink).toBeInTheDocument(); + expect(patientNameLink.tagName).toBe('A'); + }); + + it.skip('filters active visits based on search input', () => { + mockActiveVisits.mockImplementationOnce(() => ({ + activeVisits: [ + { id: '1', name: 'John Doe', visitType: 'Checkup', patientUuid: 'uuid1' }, + { id: '2', name: 'Some One', visitType: 'Checkup', patientUuid: 'uuid2' }, + ], + isLoading: false, + isValidating: false, + error: null, + })); + render(); + + const searchInput = screen.getByPlaceholderText('Filter table'); + fireEvent.change(searchInput, { target: { value: 'John' } }); + + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.queryByText('Some One')).toBeNull(); + }); + + it('displays empty state when there are no active visits', () => { + mockActiveVisits.mockImplementationOnce(() => ({ + activeVisits: [], + isLoading: false, + isValidating: false, + error: null, + })); + + render(); + + expect(screen.getByText('There are no active visits to display for this location.')).toBeInTheDocument(); + }); + + it('should not display the table when the data is loading', () => { + mockActiveVisits.mockImplementationOnce(() => ({ + activeVisits: undefined, + isLoading: true, + isValidating: false, + error: null, + })); + + render(); + + const expectedColumnHeaders = [/Visit Time/, /ID Number/, /Name/, /Gender/, /Age/, /Visit Type/]; + expectedColumnHeaders.forEach((header) => { + expect(screen.queryByRole('columnheader', { name: new RegExp(header, 'i') })).not.toBeInTheDocument(); + }); + }); + + it('should display the Error when there is error', () => { + mockActiveVisits.mockImplementationOnce(() => ({ + activeVisits: undefined, + isLoading: false, + isValidating: false, + error: 'Error in fetching data', + })); + + render(); + + const expectedColumnHeaders = [/Visit Time/, /ID Number/, /Name/, /Gender/, /Age/, /Visit Type/]; + expectedColumnHeaders.forEach((header) => { + expect(screen.queryByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument(); + }); + }); + + it('should display the pagination when pagination is true', () => { + mockActiveVisits.mockImplementationOnce(() => ({ + activeVisits: [ + { id: '1', name: 'John Doe', visitType: 'Checkup' }, + { id: '2', name: 'Some One', visitType: 'Checkup' }, + ], + isLoading: false, + isValidating: false, + error: null, + })); + mockedUsePagination.mockImplementationOnce((data) => ({ + currentPage: 1, + goTo: () => {}, + results: data, + paginated: true, + })); + render(); + + expect(screen.getByText(/Next Page/i)).toBeInTheDocument(); + }); +}); From d0b9f5b76f2fa08a137ea35499937baad7cb7b8b Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Sun, 27 Aug 2023 23:56:57 +0530 Subject: [PATCH 25/43] (test) Add test for `patient-search-icon.component` (#781) --- .../patient-search-icon.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 packages/esm-patient-search-app/src/patient-search-icon/patient-search-icon.test.tsx diff --git a/packages/esm-patient-search-app/src/patient-search-icon/patient-search-icon.test.tsx b/packages/esm-patient-search-app/src/patient-search-icon/patient-search-icon.test.tsx new file mode 100644 index 000000000..0cba40cf4 --- /dev/null +++ b/packages/esm-patient-search-app/src/patient-search-icon/patient-search-icon.test.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import PatientSearchLaunch from './patient-search-icon.component'; +import { isDesktop } from '@openmrs/esm-framework'; + +const isDesktopMock = isDesktop as jest.Mock; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + isDesktop: jest.fn(), + useOnClickOutside: jest.fn(), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useSearchParams: jest.fn(() => [ + { + get: jest.fn(() => 'John'), + }, + ]), +})); + +describe('PatientSearchLaunch', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders without errors', () => { + render(); + expect(screen.getByRole('button', { name: 'Search Patient' })).toBeInTheDocument(); + }); + + it('toggles search input when search button is clicked', () => { + render(); + const searchButton = screen.getByTestId('searchPatientIcon'); + + fireEvent.click(searchButton); + const searchInput = screen.getByText('Search results'); + expect(searchInput).toBeInTheDocument(); + + fireEvent.click(searchButton); + expect(searchInput).not.toBeInTheDocument(); + }); + + it('displays search input in overlay on mobile', () => { + isDesktopMock.mockReturnValue(false); + render(); + const searchButton = screen.getByTestId('searchPatientIcon'); + + fireEvent.click(searchButton); + const overlay = screen.getByText('Search results'); + expect(overlay).toBeInTheDocument(); + }); +}); From 0a29603a9df2a1cafeaa47e3429bd32487b5107e Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Mon, 28 Aug 2023 00:31:27 +0530 Subject: [PATCH 26/43] (test): Add test for `patient-list-detail.component` (#790) --- .../patient-list-detail.test.tsx | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 packages/esm-patient-list-app/src/patient-list-detail/patient-list-detail.test.tsx diff --git a/packages/esm-patient-list-app/src/patient-list-detail/patient-list-detail.test.tsx b/packages/esm-patient-list-app/src/patient-list-detail/patient-list-detail.test.tsx new file mode 100644 index 000000000..5aee11bd5 --- /dev/null +++ b/packages/esm-patient-list-app/src/patient-list-detail/patient-list-detail.test.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { usePatientListDetails, usePatientListMembers } from '../api/hooks'; +import { showToast } from '@openmrs/esm-framework'; +import { deletePatientList } from '../api/api-remote'; +import PatientListDetailComponent from './patient-list-detail.component'; + +const mockedUsePatientListDetails = usePatientListDetails as jest.Mock; +const mockedUsePatientListMembers = usePatientListMembers as jest.Mock; +const mockedDeletePatientList = deletePatientList as jest.Mock; + +jest.mock('../api/hooks', () => ({ + usePatientListDetails: jest.fn(), + usePatientListMembers: jest.fn(), +})); + +jest.mock('../api/api-remote'); + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + showToast: jest.fn(), + navigate: jest.fn(), + ExtensionSlot: jest.fn(), +})); + +const mockedPatientListDetails = { + name: 'Test Patient List', + description: 'This is a test patient list', + size: 5, + startDate: '2023-08-14', +}; + +const mockedPatientListMembers = [ + { + patient: { + person: { + display: 'John Doe', + gender: 'Male', + }, + identifiers: [ + { + identifier: '100GEJ', + }, + ], + uuid: '7cd38a6d-377e-491b-8284-b04cf8b8c6d8', + }, + startDate: '2023-08-10', + }, +]; + +describe('PatientListDetailComponent', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedUsePatientListDetails.mockReturnValue({ + data: mockedPatientListDetails, + }); + + mockedUsePatientListMembers.mockReturnValue({ + data: mockedPatientListMembers, + }); + + mockedDeletePatientList.mockResolvedValue({}); + }); + + it('renders patient list details', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('Test Patient List')).toBeInTheDocument(); + expect(screen.getByText('This is a test patient list')).toBeInTheDocument(); + }); + }); + + it('displays patient list members', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('Male')).toBeInTheDocument(); + expect(screen.getByText('100GEJ')).toBeInTheDocument(); + }); + }); + + it('opens edit overlay when "Edit Name/ Description" is clicked', () => { + render(); + + userEvent.click(screen.getByText('Actions')); + const editBtn = screen.getByText('Edit Name/ Description'); + userEvent.click(editBtn); + }); + + it('deletes patient list and navigates on successful delete', async () => { + render(); + + await waitFor(() => { + userEvent.click(screen.getByText('Delete')); + }); + + await waitFor(() => { + expect(mockedDeletePatientList).toHaveBeenCalledTimes(1); + expect(showToast).toHaveBeenCalledTimes(1); + }); + }); +}); From 3dbcfdee6dc822c78610fd72602b575833a62183 Mon Sep 17 00:00:00 2001 From: CynthiaKamau Date: Wed, 30 Aug 2023 11:11:47 +0300 Subject: [PATCH 27/43] KHP3-4011 Add column to denote patient previous location in in queue (#776) --- .../active-visits/active-visits-table.component.tsx | 10 +++++++++- .../src/active-visits/active-visits-table.resource.ts | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/esm-outpatient-app/src/active-visits/active-visits-table.component.tsx b/packages/esm-outpatient-app/src/active-visits/active-visits-table.component.tsx index 06b7673c6..eeef1b6be 100644 --- a/packages/esm-outpatient-app/src/active-visits/active-visits-table.component.tsx +++ b/packages/esm-outpatient-app/src/active-visits/active-visits-table.component.tsx @@ -161,11 +161,16 @@ function ActiveVisitsTable() { }, { id: 3, + header: t('locationComingFrom', 'Coming from'), + key: 'locationComingFrom', + }, + { + id: 4, header: t('status', 'Status'), key: 'status', }, { - id: 4, + id: 5, header: t('waitTime', 'Wait time'), key: 'waitTime', }, @@ -213,6 +218,9 @@ function ActiveVisitsTable() { ), }, + locationComingFrom: { + content: {entry?.locationComingFrom}, + }, status: { content: ( diff --git a/packages/esm-outpatient-app/src/active-visits/active-visits-table.resource.ts b/packages/esm-outpatient-app/src/active-visits/active-visits-table.resource.ts index 3829b9a14..cafcb3336 100644 --- a/packages/esm-outpatient-app/src/active-visits/active-visits-table.resource.ts +++ b/packages/esm-outpatient-app/src/active-visits/active-visits-table.resource.ts @@ -71,6 +71,9 @@ export interface VisitQueueEntry { uuid: string; visit: Visit; sortWeight: number; + locationComingFrom: { + name: string; + }; } export interface MappedVisitQueueEntry { @@ -99,6 +102,7 @@ export interface MappedVisitQueueEntry { sortWeight: number; visitQueueNumber: string; identifiers: Array; + locationComingFrom: string; } interface UseVisitQueueEntries { @@ -244,6 +248,7 @@ export function useVisitQueueEntries(currServiceName: string, locationUuid: stri (e) => e.attributeType.uuid === visitQueueNumberAttributeUuid, )?.value, identifiers: visitQueueEntry.queueEntry.patient?.identifiers, + locationComingFrom: visitQueueEntry.queueEntry?.locationComingFrom?.name, }); let mappedVisitQueueEntries; @@ -310,6 +315,7 @@ export async function updateQueueEntry( }, startedAt: toDateObjectStrict(toOmrsIsoString(new Date())), sortWeight: sortWeight, + locationComingFrom: previousQueueUuid, }, }, }); From dc68fe93d98cd9fb4b44d728a9aa6a5097aab67b Mon Sep 17 00:00:00 2001 From: Donald Kibet Date: Wed, 30 Aug 2023 11:34:51 +0300 Subject: [PATCH 28/43] (fix) Resolved crash in active-visit component due to missing patientUuid on visit object (#799) --- .../active-visits.component.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/esm-active-visits-app/src/active-visits-widget/active-visits.component.tsx b/packages/esm-active-visits-app/src/active-visits-widget/active-visits.component.tsx index 6bf2042b3..2177e2285 100644 --- a/packages/esm-active-visits-app/src/active-visits-widget/active-visits.component.tsx +++ b/packages/esm-active-visits-app/src/active-visits-widget/active-visits.component.tsx @@ -247,15 +247,21 @@ const ActiveVisitsTable = () => { {rows.map((row, index) => { const visit = activeVisits.find((visit) => visit.id === row.id); + if (!visit) { + return null; + } + + const patientLink = `$\{openmrsSpaBase}/patient/${visit.patientUuid}/chart/Patient%20Summary`; + return ( - + {row.cells.map((cell) => ( - {cell.info.header === 'name' ? ( - + {cell.info.header === 'name' && visit.patientUuid ? ( + {cell.value} ) : ( From 63587079fea27aa80d98fe69f3ddc645497cec5f Mon Sep 17 00:00:00 2001 From: Makombe Kennedy Date: Thu, 31 Aug 2023 14:53:01 +0300 Subject: [PATCH 29/43] =?UTF-8?q?(fix):Appointment=20calendar=20should=20b?= =?UTF-8?q?e=20relative=20to=20the=20date=20selected=20as=E2=80=A6=20(#801?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * (fix):Appointment calendar should be relative to the date selected as appointment scheduled date * Code review * code review fix --------- Co-authored-by: Makombe --- .../appointments-calendar-view.component.tsx | 9 ++-- .../monthly/monthly-header.module.tsx | 3 +- .../appointments-header.component.tsx | 6 +-- .../appointments-metrics.component.tsx | 6 +-- .../appointments-table.resource.ts | 4 +- .../location-select-option.component.tsx | 48 +++++++++++++++++++ .../cancel-appointment.component.tsx | 6 +-- .../appointments-form.component.tsx | 46 +++++++++++------- .../src/appointments/forms/workload-helper.ts | 12 +++-- .../scheduled-appointments.component.tsx | 23 +++++---- .../esm-appointments-app/src/helpers/time.tsx | 2 +- .../appointments-table.resource.ts | 6 +-- .../src/hooks/useAppointmentList.tsx | 12 ++--- .../src/hooks/useClinicalMetrics.tsx | 14 +++--- .../hooks/usePatientAppointmentHistory.tsx | 4 +- .../src/hooks/useUnscheduledAppointments.tsx | 4 +- .../src/hooks/useVisits.tsx | 4 +- .../visit-form/visit-form.component.tsx | 6 +-- .../esm-outpatient-app/translations/am.json | 1 + .../esm-outpatient-app/translations/en.json | 1 + .../esm-outpatient-app/translations/es.json | 1 + .../esm-outpatient-app/translations/fr.json | 1 + .../esm-outpatient-app/translations/he.json | 1 + .../esm-outpatient-app/translations/km.json | 1 + 24 files changed, 150 insertions(+), 71 deletions(-) create mode 100644 packages/esm-appointments-app/src/appointments/common-components/location-select-option.component.tsx diff --git a/packages/esm-appointments-app/src/appointments-calendar/appointments-calendar-view.component.tsx b/packages/esm-appointments-app/src/appointments-calendar/appointments-calendar-view.component.tsx index 50f1eb018..5e0663e76 100644 --- a/packages/esm-appointments-app/src/appointments-calendar/appointments-calendar-view.component.tsx +++ b/packages/esm-appointments-app/src/appointments-calendar/appointments-calendar-view.component.tsx @@ -6,12 +6,13 @@ import type { CalendarType } from '../types'; import AppointmentsHeader from '../appointments-header/appointments-header.component'; import CalendarHeader from './header/calendar-header.component'; import CalendarView from './calendar-view.component'; +import { useAppointmentDate } from '../helpers'; const AppointmentsCalendarView: React.FC = () => { const { t } = useTranslation(); const [calendarView, setCalendarView] = useState('monthly'); - const [currentDate, setCurrentDate] = useState(dayjs()); - const { calendarEvents } = useAppointmentsCalendar(currentDate.toISOString(), calendarView); + const { currentAppointmentDate, setCurrentAppointmentDate } = useAppointmentDate(); + const { calendarEvents } = useAppointmentsCalendar(dayjs(currentAppointmentDate).toISOString(), calendarView); return (
@@ -20,8 +21,8 @@ const AppointmentsCalendarView: React.FC = () => {
); diff --git a/packages/esm-appointments-app/src/appointments-calendar/monthly/monthly-header.module.tsx b/packages/esm-appointments-app/src/appointments-calendar/monthly/monthly-header.module.tsx index 631332ffb..2d4c88519 100644 --- a/packages/esm-appointments-app/src/appointments-calendar/monthly/monthly-header.module.tsx +++ b/packages/esm-appointments-app/src/appointments-calendar/monthly/monthly-header.module.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Dayjs } from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import styles from './monthly-header.module.scss'; import { Button } from '@carbon/react'; import { useTranslation } from 'react-i18next'; @@ -24,6 +24,7 @@ function MonthlyHeader({ setCurrentDate: (date: Dayjs) => void; }) { const { t } = useTranslation(); + return ( <>
diff --git a/packages/esm-appointments-app/src/appointments-header/appointments-header.component.tsx b/packages/esm-appointments-app/src/appointments-header/appointments-header.component.tsx index 9beecb5b5..498ef9e32 100644 --- a/packages/esm-appointments-app/src/appointments-header/appointments-header.component.tsx +++ b/packages/esm-appointments-app/src/appointments-header/appointments-header.component.tsx @@ -6,7 +6,7 @@ import AppointmentsIllustration from './appointments-illustration.component'; import styles from './appointments-header.scss'; import { DatePicker, DatePickerInput, Dropdown, Layer } from '@carbon/react'; import dayjs from 'dayjs'; -import { changeStartDate } from '../helpers'; +import { changeStartDate, useAppointmentDate } from '../helpers'; import { useAppointmentServices } from '../hooks/useAppointmentService'; interface AppointmentHeaderProps { @@ -18,7 +18,7 @@ const AppointmentsHeader: React.FC = ({ title, onChange const { t } = useTranslation(); const session = useSession(); const datePickerRef = useRef(null); - const [appointmentDate, setDateTime] = useState(new Date()); + const { currentAppointmentDate } = useAppointmentDate(); const location = session?.sessionLocation?.display; const { serviceTypes } = useAppointmentServices(); return ( @@ -46,7 +46,7 @@ const AppointmentsHeader: React.FC = ({ title, onChange placeholder="DD-MMM-YYYY" labelText="" type="text" - value={dayjs(appointmentDate).format('DD MMM YYYY')} + value={dayjs(currentAppointmentDate).format('DD MMM YYYY')} />
diff --git a/packages/esm-appointments-app/src/appointments-metrics/appointments-metrics.component.tsx b/packages/esm-appointments-app/src/appointments-metrics/appointments-metrics.component.tsx index 9cdd6b685..aca0f909d 100644 --- a/packages/esm-appointments-app/src/appointments-metrics/appointments-metrics.component.tsx +++ b/packages/esm-appointments-app/src/appointments-metrics/appointments-metrics.component.tsx @@ -15,8 +15,8 @@ const AppointmentsMetrics: React.FC<{ serviceUuid: string }> = ({ serviceUuid }) const { totalProviders, isLoading: allAppointmentsLoading } = useAllAppointmentsByDate(); const { totalScheduledAppointments } = useScheduledAppointment(serviceUuid); - const startDate = useAppointmentDate(); - const formattedStartDate = formatDate(parseDate(startDate), { mode: 'standard', time: false }); + const { currentAppointmentDate } = useAppointmentDate(); + const formattedStartDate = formatDate(parseDate(currentAppointmentDate), { mode: 'standard', time: false }); const { appointmentList: arrivedAppointments } = useAppointmentList('Honoured'); const { appointmentList: pendingAppointments } = useAppointmentList('Pending'); @@ -43,7 +43,7 @@ const AppointmentsMetrics: React.FC<{ serviceUuid: string }> = ({ serviceUuid }) value={totalScheduledAppointments} headerLabel={t('scheduledAppointments', 'Scheduled appointments')} count={{ pendingAppointments: filteredPendingAppointments, arrivedAppointments: filteredArrivedAppointments }} - appointmentDate={startDate} + appointmentDate={currentAppointmentDate} /> ; +} + +interface LocationOptions { + uuid?: string; + display?: string; +} + +const LocationSelectOption: React.FC = ({ selectedLocation, defaultFacility, locations }) => { + const { t } = useTranslation(); + if (!selectedLocation) { + return ; + } + + if (defaultFacility && Object.keys(defaultFacility).length !== 0) { + return ( + + {defaultFacility?.display} + + ); + } + + if (locations && locations.length > 0) { + return ( + <> + {locations.map((location) => ( + + {location.display} + + ))} + + ); + } + + return null; +}; + +export default LocationSelectOption; diff --git a/packages/esm-appointments-app/src/appointments/forms/cancel-form/cancel-appointment.component.tsx b/packages/esm-appointments-app/src/appointments/forms/cancel-form/cancel-appointment.component.tsx index 049a796cf..843a424f5 100644 --- a/packages/esm-appointments-app/src/appointments/forms/cancel-form/cancel-appointment.component.tsx +++ b/packages/esm-appointments-app/src/appointments/forms/cancel-form/cancel-appointment.component.tsx @@ -19,7 +19,7 @@ const CancelAppointment: React.FC = ({ appointment }) => const session = useSession(); const [selectedLocation, setSelectedLocation] = useState(appointment.location); const [reason, setReason] = useState(''); - const startDate = useAppointmentDate(); + const { currentAppointmentDate } = useAppointmentDate(); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { @@ -38,8 +38,8 @@ const CancelAppointment: React.FC = ({ appointment }) => description: t('appointmentNowVisible', 'It has been cancelled successfully'), title: t('appointmentCancelled', 'Appointment cancelled'), }); - mutate(`/ws/rest/v1/appointment/appointmentStatus?forDate=${startDate}&status=Scheduled`); - mutate(`/ws/rest/v1/appointment/appointmentStatus?forDate=${startDate}&status=Cancelled`); + mutate(`/ws/rest/v1/appointment/appointmentStatus?forDate=${currentAppointmentDate}&status=Scheduled`); + mutate(`/ws/rest/v1/appointment/appointmentStatus?forDate=${currentAppointmentDate}&status=Cancelled`); closeOverlay(); } else { showNotification({ diff --git a/packages/esm-appointments-app/src/appointments/forms/create-edit-form/appointments-form.component.tsx b/packages/esm-appointments-app/src/appointments/forms/create-edit-form/appointments-form.component.tsx index e457d5c19..77df4feea 100644 --- a/packages/esm-appointments-app/src/appointments/forms/create-edit-form/appointments-form.component.tsx +++ b/packages/esm-appointments-app/src/appointments/forms/create-edit-form/appointments-form.component.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import { @@ -32,6 +32,7 @@ import { showNotification, showToast, ConfigObject, + useSession, } from '@openmrs/esm-framework'; import first from 'lodash-es/first'; @@ -50,6 +51,8 @@ import { import { useInitialAppointmentFormValue, PatientAppointment } from '../useInitialFormValues'; import { useCalendarDistribution } from '../workload-helper'; import WorkloadCard from '../workload.component'; +import { useDefaultLoginLocation } from '../../../hooks/useDefaultLocation'; +import LocationSelectOption from '../../common-components/location-select-option.component'; interface AppointmentFormProps { appointment?: MappedAppointment; @@ -63,6 +66,7 @@ const AppointmentForm: React.FC = ({ appointment, patientU const [patientAppointment, setPatientAppointment] = useState(initialAppointmentFormValues); const { patient, isLoading } = usePatient(patientUuid ?? patientAppointment.patientUuid); const locations = useLocations(); + const sessionUser = useSession(); const { providers } = useProviders(); const { services } = useServices(); const [isSubmitting, setIsSubmitting] = useState(false); @@ -71,11 +75,22 @@ const AppointmentForm: React.FC = ({ appointment, patientU const calendarWorkload = useCalendarDistribution( patientAppointment.serviceUuid, selectedTab === 0 ? 'week' : 'month', + patientAppointment.visitDate, ); - const appointmentStartDate = useAppointmentDate(); + const { currentAppointmentDate } = useAppointmentDate(); + const [selectedLocation, setSelectedLocation] = useState(''); + const { defaultFacility, isLoading: loadingDefaultFacility } = useDefaultLoginLocation(); const appointmentService = services?.find(({ uuid }) => uuid === patientAppointment.serviceUuid); + useEffect(() => { + if (locations?.length && sessionUser) { + setSelectedLocation(sessionUser?.sessionLocation?.uuid); + } else if (!loadingDefaultFacility && defaultFacility) { + setSelectedLocation(defaultFacility?.uuid); + } + }, [locations, sessionUser, loadingDefaultFacility]); + const handleSubmit = async () => { const [hours, minutes] = convertTime12to24(patientAppointment.startDateTime, patientAppointment.timeFormat); const startDatetime = toAppointmentDateTime(patientAppointment.visitDate, hours, minutes); @@ -107,11 +122,11 @@ const AppointmentForm: React.FC = ({ appointment, patientU title: t('appointmentScheduled', 'Appointment scheduled'), }); setIsSubmitting(false); - mutate(`/ws/rest/v1/appointment/appointmentStatus?forDate=${appointmentStartDate}&status=Scheduled`); - mutate(`/ws/rest/v1/appointment/appointmentStatus?forDate=${appointmentStartDate}&status=CheckedIn`); - mutate(`/ws/rest/v1/appointment/all?forDate=${appointmentStartDate}`); - mutate(`/ws/rest/v1/appointment/appointmentStatus?status=Scheduled&forDate=${appointmentStartDate}`); - mutate(`/ws/rest/v1/appointment/appointmentStatus?status=Pending&forDate=${appointmentStartDate}`); + mutate(`/ws/rest/v1/appointment/appointmentStatus?forDate=${currentAppointmentDate}&status=Scheduled`); + mutate(`/ws/rest/v1/appointment/appointmentStatus?forDate=${currentAppointmentDate}&status=CheckedIn`); + mutate(`/ws/rest/v1/appointment/all?forDate=${currentAppointmentDate}`); + mutate(`/ws/rest/v1/appointment/appointmentStatus?status=Scheduled&forDate=${currentAppointmentDate}`); + mutate(`/ws/rest/v1/appointment/appointmentStatus?status=Pending&forDate=${currentAppointmentDate}`); closeOverlay(); } }, @@ -159,15 +174,14 @@ const AppointmentForm: React.FC = ({ appointment, patientU labelText={t('selectLocation', 'Select a location')} id="location" invalidText="Required" - value={patientAppointment.locationUuid} - className={styles.inputContainer} - onChange={(event) => setPatientAppointment({ ...patientAppointment, locationUuid: event.target.value })}> - {locations?.length > 0 && - locations.map((location) => ( - - {location.display} - - ))} + value={selectedLocation} + defaultSelected={selectedLocation} + onChange={(event) => setSelectedLocation(event.target.value)}> +