From 81d86498ba667c3bf8f69cafbf0bf3bfe7f5e661 Mon Sep 17 00:00:00 2001 From: Jayasanka Weerasinghe <33048395+jayasanka-sack@users.noreply.github.com> Date: Thu, 4 Jul 2024 19:20:14 +0100 Subject: [PATCH 01/90] (test) Fix flaky Lab order E2E test (#1894) Fix Flaky Lab order E2E test --- e2e/specs/lab-orders.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e/specs/lab-orders.spec.ts b/e2e/specs/lab-orders.spec.ts index 466ed5327e..f2110f0362 100644 --- a/e2e/specs/lab-orders.spec.ts +++ b/e2e/specs/lab-orders.spec.ts @@ -14,6 +14,7 @@ test.beforeEach(async ({ api }) => { test('Record a lab order', async ({ page }) => { const ordersPage = new OrdersPage(page); + const orderBasket = page.locator('[data-extension-slot-name="order-basket-slot"]'); await test.step('When I visit the orders page', async () => { await ordersPage.goTo(patient.uuid); @@ -24,7 +25,7 @@ test('Record a lab order', async ({ page }) => { }); await test.step('And I click the `Add +` button on the Lab orders tile', async () => { - await page.getByRole('button', { name: /add/i }).nth(1).click(); + await orderBasket.getByRole('button', { name: /add/i }).nth(1).click(); }); await test.step('Then I type `Blood urea nitrogen` into the search bar', async () => { From 89ffc4a5ae3d05802f6e8078556f7f43298492d0 Mon Sep 17 00:00:00 2001 From: Dennis Kigen Date: Fri, 5 Jul 2024 13:26:26 +0300 Subject: [PATCH 02/90] (fix) Fixup conditions form test --- .../src/conditions/conditions-form.test.tsx | 75 ++++++++----------- .../conditions-widget.component.tsx | 8 +- 2 files changed, 34 insertions(+), 49 deletions(-) diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx index 98562d2d6e..9f8cbb03cd 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx @@ -1,30 +1,13 @@ import React from 'react'; import dayjs from 'dayjs'; -import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; import { type FetchResponse, openmrsFetch, showSnackbar } from '@openmrs/esm-framework'; import { mockFhirConditionsResponse, searchedCondition } from '__mocks__'; import { getByTextWithMarkup, mockPatient } from 'tools'; import { createCondition, useConditionsSearch } from './conditions.resource'; import ConditionsForm from './conditions-form.workspace'; -jest.mock('zod', () => { - const originalModule = jest.requireActual('zod'); - const mockedZod = { - ...originalModule, - z: { - ...originalModule.z, - schema: jest.fn(() => ({ - safeParse: jest.fn(() => ({ - success: true, - data: {}, - })), - })), - }, - }; - return mockedZod; -}); - const utc = require('dayjs/plugin/utc'); dayjs.extend(utc); @@ -42,8 +25,6 @@ const mockUseConditionsSearch = useConditionsSearch as jest.Mock; const mockShowSnackbar = showSnackbar as jest.Mock; const mockOpenmrsFetch = jest.mocked(openmrsFetch); -jest.mock('lodash-es/debounce', () => jest.fn((fn) => fn)); - jest.mock('@openmrs/esm-framework', () => { const originalModule = jest.requireActual('@openmrs/esm-framework'); @@ -76,15 +57,16 @@ describe('Conditions form', () => { it('renders the conditions form with all the relevant fields and values', () => { renderConditionsForm(); - expect(screen.getByRole('group', { name: /Condition/i })).toBeInTheDocument(); - expect(screen.getByRole('textbox', { name: /Onset date/i })).toBeInTheDocument(); - expect(screen.getByRole('group', { name: /Clinical status/i })).toBeInTheDocument(); - expect(screen.getByRole('searchbox', { name: /Enter condition/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Clear search input/i })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: 'Active' })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: 'Active' })).not.toBeChecked(); - expect(screen.getByRole('radio', { name: 'Inactive' })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: 'Inactive' })).not.toBeChecked(); + expect(screen.getByRole('group', { name: /condition/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /onset date/i })).toBeInTheDocument(); + expect(screen.getByRole('group', { name: /clinical status/i })).toBeInTheDocument(); + expect(screen.getByRole('searchbox', { name: /enter condition/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /clear search input/i })).toBeInTheDocument(); + + expect(screen.getByLabelText(/^active/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^active/i)).not.toBeChecked(); + expect(screen.getByLabelText(/inactive/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/inactive/i)).not.toBeChecked(); const cancelButton = screen.getByRole('button', { name: /Cancel/i }); const submitButton = screen.getByRole('button', { name: /Save & close/i }); @@ -97,38 +79,42 @@ describe('Conditions form', () => { const user = userEvent.setup(); renderConditionsForm(); - const cancelButton = screen.getByRole('button', { name: /Cancel/i }); + const cancelButton = screen.getByRole('button', { name: /cancel/i }); await user.click(cancelButton); expect(testProps.closeWorkspace).toHaveBeenCalledTimes(1); }); - it('setting the status of a condition to "inactive" reveals an input for recording the end date', async () => { + it('setting the status of a condition to "inactive" reveals the end date input field', async () => { const user = userEvent.setup(); renderConditionsForm(); - expect(screen.getByText('Condition')).toBeInTheDocument(); - await user.click(screen.getByRole('radio', { name: /Inactive/i })); - expect(screen.getByLabelText(/End date/i)).toBeInTheDocument(); + await user.click(screen.getByLabelText(/^active/i)); + expect(screen.queryByLabelText(/end date/i)).not.toBeInTheDocument(); + + await user.click(screen.getByLabelText(/inactive/i)); + expect(screen.getByLabelText(/end date/i)).toBeInTheDocument(); }); - it('renders a list of related condition concepts when the user types in the searchbox', async () => { + it('renders a list of matching conditions when the user types a query into the searchbox', async () => { const user = userEvent.setup(); renderConditionsForm(); const conditionSearchInput = screen.getByRole('searchbox', { name: /enter condition/i }); expect(screen.queryByRole('menuitem', { name: /Headache/i })).not.toBeInTheDocument(); expect(screen.queryByDisplayValue('Headache')).not.toBeInTheDocument(); + await user.type(conditionSearchInput, 'Headache'); expect(screen.getByDisplayValue(/headache/i)).toBeInTheDocument(); }); - it('renders an error message when no matching conditions are found', async () => { + it('renders an error message when there are no conditions that match the search query', async () => { const user = userEvent.setup(); renderConditionsForm(); const conditionSearchInput = screen.getByRole('searchbox', { name: /enter condition/i }); expect(screen.queryByRole('menuitem', { name: /Post-acute sequelae of COVID-19/i })).not.toBeInTheDocument(); expect(screen.queryByDisplayValue(/Post-acute sequelae of COVID-19/i)).not.toBeInTheDocument(); + await user.type(conditionSearchInput, 'Post-acute sequelae of COVID-19'); expect(getByTextWithMarkup('No results for "Post-acute sequelae of COVID-19"')).toBeInTheDocument(); }); @@ -159,13 +145,12 @@ describe('Conditions form', () => { await user.type(onsetDateInput, '2020-05-05'); await user.click(submitButton); - // TODO: Figure out why this isn't working - // expect(mockShowSnackbar).toHaveBeenCalled(); - // expect(mockShowSnackbar).toHaveBeenCalledWith({ - // kind: 'success', - // subtitle: 'It is now visible on the Conditions page', - // title: 'Condition saved', - // }); + expect(mockShowSnackbar).toHaveBeenCalled(); + expect(mockShowSnackbar).toHaveBeenCalledWith({ + kind: 'success', + subtitle: 'It is now visible on the Conditions page', + title: 'Condition saved', + }); }); it('renders an error notification if there was a problem recording a condition', async () => { @@ -222,7 +207,7 @@ describe('Conditions form', () => { expect(screen.getByText(/a clinical status is required/i)).toBeInTheDocument(); - await user.click(screen.getByRole('radio', { name: 'Active' })); + await user.click(screen.getByLabelText(/^active/i)); await user.click(submitButton); expect(screen.queryByText(/a condition is required/i)).not.toBeInTheDocument(); @@ -257,7 +242,7 @@ describe('Conditions form', () => { expect(screen.queryByRole('searchbox', { name: /Enter condition/i })).not.toBeInTheDocument(); - const inactiveStatusInput = screen.getByRole('radio', { name: 'Inactive' }); + const inactiveStatusInput = screen.getByLabelText(/inactive/i); const submitButton = screen.getByRole('button', { name: /save & close/i }); await user.click(inactiveStatusInput); diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx index 0ebe826472..3775657b7a 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx @@ -311,7 +311,7 @@ const ConditionsWidget: React.FC = ({ render={({ field: { onChange, value, onBlur } }) => ( = ({ function RequiredFieldLabel({ label, t }: RequiredFieldLabelProps) { return ( - <> - {label} + + {label} * - + ); } From 5bbc916d0f4faf31352d326147b8c8ff39a299a0 Mon Sep 17 00:00:00 2001 From: Dennis Kigen Date: Fri, 5 Jul 2024 20:30:48 +0300 Subject: [PATCH 03/90] (fix) Remove deprecated workspace components and fix tests (#1896) --- .../src/form-renderer/form-renderer.test.tsx | 1 + .../allergies-form/allergy-form.test.tsx | 6 +- .../patient-chart/patient-chart.component.tsx | 2 - .../src/side-nav/side-menu.test.tsx | 2 +- .../src/visit/past-visit-overview.test.tsx | 1 + .../src/visit/visit-form/visit-form.test.tsx | 2 + .../src/conditions/conditions-form.test.tsx | 1 + .../src/flags/flags-highlight-bar.test.tsx | 2 +- .../src/flags/flags-list.test.tsx | 3 +- .../src/flags/flags.test.tsx | 2 +- .../src/forms/form-entry.test.tsx | 1 + .../src/forms/forms-dashboard.test.tsx | 1 + .../immunizations/immunizations-form.test.tsx | 2 + .../add-lab-order/add-lab-order.test.tsx | 1 + .../add-lab-order/add-lab-order.workspace.tsx | 1 + .../patient-list-details.workspace.test.tsx | 1 + .../add-drug-order/add-drug-order.test.tsx | 1 + .../src/notes/visit-notes-form.test.tsx | 1 + .../src/programs/programs-form.test.tsx | 1 + .../vitals-biometrics-form.test.tsx | 4 + yarn.lock | 200 +++++++++--------- 21 files changed, 128 insertions(+), 108 deletions(-) diff --git a/packages/esm-form-engine-app/src/form-renderer/form-renderer.test.tsx b/packages/esm-form-engine-app/src/form-renderer/form-renderer.test.tsx index 74d043c3c8..02c4ea235e 100644 --- a/packages/esm-form-engine-app/src/form-renderer/form-renderer.test.tsx +++ b/packages/esm-form-engine-app/src/form-renderer/form-renderer.test.tsx @@ -23,6 +23,7 @@ describe('FormRenderer', () => { closeWorkspace: jest.fn(), closeWorkspaceWithSavedChanges: jest.fn(), promptBeforeClosing: jest.fn(), + setTitle: jest.fn(), }; test('renders FormError component when there is an error', () => { diff --git a/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.test.tsx b/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.test.tsx index 7dbd2f70fb..cf8b0799a2 100644 --- a/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.test.tsx +++ b/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.test.tsx @@ -11,9 +11,9 @@ import { useAllergicReactions, updatePatientAllergy, } from './allergy-form.resource'; -import AllergyForm from './allergy-form.workspace'; -import { AllergenType, ReactionSeverity } from '../../types'; +import { AllergenType } from '../../types'; import { mockAllergy } from '__mocks__'; +import AllergyForm from './allergy-form.workspace'; const mockSaveAllergy = saveAllergy as jest.Mock>; const mockUseAllergens = useAllergens as jest.Mock; @@ -328,6 +328,7 @@ function renderAllergyForm() { formContext: 'creating' as 'creating' | 'editing', patient: mockPatient, patientUuid: mockPatient.id, + setTitle: jest.fn(), }; render(); @@ -342,6 +343,7 @@ function renderEditAllergyForm() { formContext: 'editing' as 'creating' | 'editing', patient: mockPatient, patientUuid: mockPatient.id, + setTitle: jest.fn(), }; render(); diff --git a/packages/esm-patient-chart-app/src/patient-chart/patient-chart.component.tsx b/packages/esm-patient-chart-app/src/patient-chart/patient-chart.component.tsx index f369e26363..9cf8f31d05 100644 --- a/packages/esm-patient-chart-app/src/patient-chart/patient-chart.component.tsx +++ b/packages/esm-patient-chart-app/src/patient-chart/patient-chart.component.tsx @@ -1,10 +1,8 @@ import React, { useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; import { - ActionMenu, ExtensionSlot, WorkspaceContainer, - WorkspaceWindow, setCurrentVisit, setLeftNav, unsetLeftNav, diff --git a/packages/esm-patient-chart-app/src/side-nav/side-menu.test.tsx b/packages/esm-patient-chart-app/src/side-nav/side-menu.test.tsx index ece38790a7..d9ec422d00 100644 --- a/packages/esm-patient-chart-app/src/side-nav/side-menu.test.tsx +++ b/packages/esm-patient-chart-app/src/side-nav/side-menu.test.tsx @@ -44,5 +44,5 @@ describe('sidemenu', () => { }); function renderSideMenu() { - return render(); + render(); } diff --git a/packages/esm-patient-chart-app/src/visit/past-visit-overview.test.tsx b/packages/esm-patient-chart-app/src/visit/past-visit-overview.test.tsx index 8a84914d59..cd04934e09 100644 --- a/packages/esm-patient-chart-app/src/visit/past-visit-overview.test.tsx +++ b/packages/esm-patient-chart-app/src/visit/past-visit-overview.test.tsx @@ -10,6 +10,7 @@ const testProps = { closeWorkspaceWithSavedChanges: jest.fn(), patientUuid: mockPatient.id, promptBeforeClosing: jest.fn(), + setTitle: jest.fn(), }; const mockPastVisits = { diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx index a60d4d2216..2094acb091 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx @@ -44,6 +44,7 @@ const visitAttributes = { const mockCloseWorkspace = jest.fn(); const mockPromptBeforeClosing = jest.fn(); +const mockSetTitle = jest.fn(); const testProps = { patientUuid: mockPatient.id, @@ -51,6 +52,7 @@ const testProps = { closeWorkspaceWithSavedChanges: mockCloseWorkspace, promptBeforeClosing: mockPromptBeforeClosing, showVisitEndDateTimeFields: false, + setTitle: mockSetTitle, }; const mockSaveVisit = saveVisit as jest.Mock; diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx index 9f8cbb03cd..51d86e731e 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx @@ -18,6 +18,7 @@ const testProps = { patientUuid: mockPatient.id, promptBeforeClosing: jest.fn(), formContext: 'creating' as 'creating' | 'editing', + setTitle: jest.fn(), }; const mockCreateCondition = createCondition as jest.Mock; diff --git a/packages/esm-patient-flags-app/src/flags/flags-highlight-bar.test.tsx b/packages/esm-patient-flags-app/src/flags/flags-highlight-bar.test.tsx index 8ff296cabb..c1cf45f9a8 100644 --- a/packages/esm-patient-flags-app/src/flags/flags-highlight-bar.test.tsx +++ b/packages/esm-patient-flags-app/src/flags/flags-highlight-bar.test.tsx @@ -66,5 +66,5 @@ it('renders a highlights bar showing a summary of the available flags', async () }); function renderFlagsHighlightBar() { - return render(); + render(); } diff --git a/packages/esm-patient-flags-app/src/flags/flags-list.test.tsx b/packages/esm-patient-flags-app/src/flags/flags-list.test.tsx index 5bc759796d..5eb6d38d5c 100644 --- a/packages/esm-patient-flags-app/src/flags/flags-list.test.tsx +++ b/packages/esm-patient-flags-app/src/flags/flags-list.test.tsx @@ -40,12 +40,13 @@ it('renders an Edit form that enables users to toggle flags on or off', async () }); function renderFlagsList() { - return render( + render( , ); } diff --git a/packages/esm-patient-flags-app/src/flags/flags.test.tsx b/packages/esm-patient-flags-app/src/flags/flags.test.tsx index 95ce9b94ab..44df946a72 100644 --- a/packages/esm-patient-flags-app/src/flags/flags.test.tsx +++ b/packages/esm-patient-flags-app/src/flags/flags.test.tsx @@ -53,5 +53,5 @@ it('renders flags in the patient flags slot', async () => { }); function renderFlags() { - return render( {}} showHighlightBar={false} />); + render( {}} showHighlightBar={false} />); } diff --git a/packages/esm-patient-forms-app/src/forms/form-entry.test.tsx b/packages/esm-patient-forms-app/src/forms/form-entry.test.tsx index 819d964d3f..ccff53f107 100644 --- a/packages/esm-patient-forms-app/src/forms/form-entry.test.tsx +++ b/packages/esm-patient-forms-app/src/forms/form-entry.test.tsx @@ -63,6 +63,7 @@ function renderFormEntry() { patientUuid: mockPatient.id, formInfo: { formUuid: 'some-form-uuid' }, mutateForm: jest.fn(), + setTitle: jest.fn(), }; render(); diff --git a/packages/esm-patient-forms-app/src/forms/forms-dashboard.test.tsx b/packages/esm-patient-forms-app/src/forms/forms-dashboard.test.tsx index 7a71e920cd..b2f7b86dcd 100644 --- a/packages/esm-patient-forms-app/src/forms/forms-dashboard.test.tsx +++ b/packages/esm-patient-forms-app/src/forms/forms-dashboard.test.tsx @@ -58,6 +58,7 @@ function renderFormDashboard() { closeWorkspace={jest.fn()} closeWorkspaceWithSavedChanges={jest.fn()} patientUuid="" + setTitle={jest.fn()} />, ); } diff --git a/packages/esm-patient-immunizations-app/src/immunizations/immunizations-form.test.tsx b/packages/esm-patient-immunizations-app/src/immunizations/immunizations-form.test.tsx index 92e9ec2578..c2d9d4e934 100644 --- a/packages/esm-patient-immunizations-app/src/immunizations/immunizations-form.test.tsx +++ b/packages/esm-patient-immunizations-app/src/immunizations/immunizations-form.test.tsx @@ -11,6 +11,7 @@ const mockCloseWorkspace = jest.fn(); const mockCloseWorkspaceWithSavedChanges = jest.fn(); const mockPromptBeforeClosing = jest.fn(); const mockSavePatientImmunization = savePatientImmunization as jest.Mock; +const mockSetTitle = jest.fn(); jest.mock('@openmrs/esm-framework', () => ({ ...jest.requireActual('@openmrs/esm-framework'), @@ -86,6 +87,7 @@ const testProps = { closeWorkspace: mockCloseWorkspace, closeWorkspaceWithSavedChanges: mockCloseWorkspaceWithSavedChanges, promptBeforeClosing: mockPromptBeforeClosing, + setTitle: mockSetTitle, }; describe('Immunizations Form', () => { diff --git a/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx b/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx index 486e7ccd04..08e48d4f5e 100644 --- a/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx +++ b/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx @@ -64,6 +64,7 @@ function renderAddLabOrderWorkspace() { closeWorkspaceWithSavedChanges={mockCloseWorkspaceWithSavedChanges} promptBeforeClosing={mockPromptBeforeClosing} patientUuid={ptUuid} + setTitle={jest.fn()} />, ); return { mockCloseWorkspace, mockPromptBeforeClosing, mockCloseWorkspaceWithSavedChanges, ...renderResult }; diff --git a/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.workspace.tsx b/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.workspace.tsx index fa5ad390f0..5eac2e284b 100644 --- a/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.workspace.tsx +++ b/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.workspace.tsx @@ -77,6 +77,7 @@ export default function AddLabOrderWorkspace({ closeWorkspace={closeWorkspace} closeWorkspaceWithSavedChanges={closeWorkspaceWithSavedChanges} promptBeforeClosing={promptBeforeClosing} + setTitle={() => {}} /> )} diff --git a/packages/esm-patient-lists-app/src/workspaces/patient-list-details.workspace.test.tsx b/packages/esm-patient-lists-app/src/workspaces/patient-list-details.workspace.test.tsx index b66e4a848c..8ea4e2abc9 100644 --- a/packages/esm-patient-lists-app/src/workspaces/patient-list-details.workspace.test.tsx +++ b/packages/esm-patient-lists-app/src/workspaces/patient-list-details.workspace.test.tsx @@ -20,6 +20,7 @@ const testProps = { startDate: '2023-11-14T23:45:51.000+0000', type: 'My List', }, + setTitle: jest.fn(), }; const mockPatientListData = [ diff --git a/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx b/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx index be9f0cb08f..4aa8e0b09e 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx +++ b/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx @@ -55,6 +55,7 @@ function renderDrugSearch() { closeWorkspaceWithSavedChanges={({ onWorkspaceClose }) => onWorkspaceClose()} promptBeforeClosing={() => false} patientUuid={'mock-patient-uuid'} + setTitle={jest.fn()} />, ); } diff --git a/packages/esm-patient-notes-app/src/notes/visit-notes-form.test.tsx b/packages/esm-patient-notes-app/src/notes/visit-notes-form.test.tsx index ba9250f59c..3536f391ca 100644 --- a/packages/esm-patient-notes-app/src/notes/visit-notes-form.test.tsx +++ b/packages/esm-patient-notes-app/src/notes/visit-notes-form.test.tsx @@ -19,6 +19,7 @@ const testProps = { closeWorkspace: jest.fn(), closeWorkspaceWithSavedChanges: jest.fn(), promptBeforeClosing: jest.fn(), + setTitle: jest.fn(), }; const mockFetchDiagnosisConceptsByName = fetchDiagnosisConceptsByName as jest.Mock; diff --git a/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx b/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx index 35c1f208fe..328469dba4 100644 --- a/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx +++ b/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx @@ -14,6 +14,7 @@ const testProps = { closeWorkspaceWithSavedChanges: jest.fn(), patientUuid: mockPatient.id, promptBeforeClosing: jest.fn(), + setTitle: jest.fn(), }; const mockCreateErrorHandler = createErrorHandler as jest.Mock; diff --git a/packages/esm-patient-vitals-app/src/vitals-biometrics-form/vitals-biometrics-form.test.tsx b/packages/esm-patient-vitals-app/src/vitals-biometrics-form/vitals-biometrics-form.test.tsx index e059e6eb48..c254110020 100644 --- a/packages/esm-patient-vitals-app/src/vitals-biometrics-form/vitals-biometrics-form.test.tsx +++ b/packages/esm-patient-vitals-app/src/vitals-biometrics-form/vitals-biometrics-form.test.tsx @@ -310,7 +310,9 @@ function renderForm() { patientUuid: mockPatient.id, promptBeforeClosing: () => {}, formContext: 'creating' as 'creating' | 'editing', + setTitle: jest.fn(), }; + render(); } @@ -324,6 +326,8 @@ function renderEditVitalsBiometricsForm() { vitalsBiometrics: formattedVitalsAndBiometrics, formContext: 'editing' as 'creating' | 'editing', formType: 'vitals' as 'vitals' | 'biometrics', + setTitle: jest.fn(), }; + render(); } diff --git a/yarn.lock b/yarn.lock index 093ab6240f..6c6bca3a50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4058,9 +4058,9 @@ __metadata: languageName: node linkType: hard -"@openmrs/esm-api@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-api@npm:5.6.1-pre.1955" +"@openmrs/esm-api@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-api@npm:5.6.1-pre.2033" dependencies: "@types/fhir": "npm:0.0.31" lodash-es: "npm:^4.17.21" @@ -4069,17 +4069,17 @@ __metadata: "@openmrs/esm-error-handling": 5.x "@openmrs/esm-navigation": 5.x "@openmrs/esm-offline": 5.x - checksum: 10/9a3d896b83021942c8653c52fbe919f345498749fafa98de2946aa7e3f79bd166bad4a9b02a10f34916e5fa4905217b53935f867d91069b51e313cc5896ff847 + checksum: 10/06400186707f9a30a24fb0a685882fcbf51ac943a937a4f1c8acca0ac2de084b59ca3debca944ae8834d17c41d4155d73daa0d5e664caab5365806a50a2128bb languageName: node linkType: hard -"@openmrs/esm-app-shell@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-app-shell@npm:5.6.1-pre.1955" +"@openmrs/esm-app-shell@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-app-shell@npm:5.6.1-pre.2033" dependencies: "@carbon/react": "npm:~1.37.0" - "@openmrs/esm-framework": "npm:5.6.1-pre.1955" - "@openmrs/esm-styleguide": "npm:5.6.1-pre.1955" + "@openmrs/esm-framework": "npm:5.6.1-pre.2033" + "@openmrs/esm-styleguide": "npm:5.6.1-pre.2033" dayjs: "npm:^1.10.4" dexie: "npm:^3.0.3" html-webpack-plugin: "npm:^5.5.0" @@ -4104,57 +4104,57 @@ __metadata: workbox-strategies: "npm:^6.1.5" workbox-webpack-plugin: "npm:^6.1.5" workbox-window: "npm:^6.1.5" - checksum: 10/4740564c41188c49fabedc99720c93c3e24d0030afdf1f923de5b80188f53119e84f5da3e0314fc8c3e109714123bbd98e779cfda4fd90247640eb5211df3e57 + checksum: 10/900ebbb33921789ba12102a1f9f430e2318debd5d304224a918624cbe8e2ce133a91faa66ed200bac14bc20c3158f13b1956fefd6a5178ef3bd3fc356dbc2aa0 languageName: node linkType: hard -"@openmrs/esm-config@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-config@npm:5.6.1-pre.1955" +"@openmrs/esm-config@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-config@npm:5.6.1-pre.2033" dependencies: ramda: "npm:^0.26.1" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-state": 5.x single-spa: 5.x - checksum: 10/98ca2ebc3ed687be3e9330cc9c4308685f3a31c12024fa0c4f2af7c1cb2c9aa6f7277085a567076a894c3c4e5d7a022eb7c0643022ab7dc759a02682e16b0564 + checksum: 10/afff9c474836bc5bb8968fcaf29a0e89e0c2a36054cbe253548950aa815140c4c16e1ba4a948f3d042d28871e4cb07fc15875dd768918e44250f38c53b7e7901 languageName: node linkType: hard -"@openmrs/esm-context@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-context@npm:5.6.1-pre.1955" +"@openmrs/esm-context@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-context@npm:5.6.1-pre.2033" dependencies: immer: "npm:^10.0.4" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-state": 5.x - checksum: 10/3dc3bdf06afe1597ab897e637657e5219ed4c956451e2ef47fdc92c1aa0b6d30037299aa2006ebf79a9b733e0f1cb98f02b0aeef24cdcc7dd2daaf61154d9e83 + checksum: 10/ffee7d5233f386d737a2ac966c888b247e4923a3e168317e12fecacfac365bf1dcc41ccd0a0f798ea55a7cfc0a43fc5b222b94160f00b6b71004bb8a8b757893 languageName: node linkType: hard -"@openmrs/esm-dynamic-loading@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-dynamic-loading@npm:5.6.1-pre.1955" +"@openmrs/esm-dynamic-loading@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-dynamic-loading@npm:5.6.1-pre.2033" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-translations": 5.x - checksum: 10/9b3df1fd2280e31209dd478aaebbe10b6ca66fd2ba228763c20d02b81ce4f3109ef1556e90de30ea771c83cf20b589ca8a02c01c8b055ed1d2ef1945bebd47e0 + checksum: 10/4d7337ff67101c04ff8a093e9a24263d5e245cf320f70be6988f748c8eee7184fc6ad59f30bdf8083c7a603e62732b72a41cc6894b4c8c2421c2d3c4e226c0d0 languageName: node linkType: hard -"@openmrs/esm-error-handling@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-error-handling@npm:5.6.1-pre.1955" +"@openmrs/esm-error-handling@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-error-handling@npm:5.6.1-pre.2033" peerDependencies: "@openmrs/esm-globals": 5.x - checksum: 10/7aa446bd5b958d6174619e6ca3064303036dfba65d9297785922d6f158ddecb5a5bf1345947999d6e3ab3edd8d3d1a25e3c66576a25f3cf724252d13d1c9bab7 + checksum: 10/6a99c9bef00bae01b55975f20fa267be1fa044fda8dc7e96a3862453ec8f2bf92790a88518bf1a9d8be58bf34dcf40f68b18eaa51fbd0f5830b39172e5c709f4 languageName: node linkType: hard -"@openmrs/esm-extensions@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-extensions@npm:5.6.1-pre.1955" +"@openmrs/esm-extensions@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-extensions@npm:5.6.1-pre.2033" dependencies: lodash-es: "npm:^4.17.21" peerDependencies: @@ -4164,20 +4164,20 @@ __metadata: "@openmrs/esm-state": 5.x "@openmrs/esm-utils": 5.x single-spa: 5.x - checksum: 10/39d4514372da38e8db5f3e96344de071b42e54f1e0c6ccd445f9b7c576db4b6343ab44b1ae28a4a9f5819ba06bb7f112864941d1a554ebac3931c3568c59de81 + checksum: 10/497d42f663017c09f2fa9e6cd8187b33c67a84105698327fcc98547fc9665ee3d5ed48ee1bb562376b554bdf88413421ced18ada76e08a0b5c751c1f89fc1248 languageName: node linkType: hard -"@openmrs/esm-feature-flags@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-feature-flags@npm:5.6.1-pre.1955" +"@openmrs/esm-feature-flags@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-feature-flags@npm:5.6.1-pre.2033" dependencies: ramda: "npm:^0.26.1" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-state": 5.x single-spa: 5.x - checksum: 10/edc838b5d23286ff3d8f3ca5f6bfae1127a606f4339c82def09b2dc68cf933c859638f664f66936cc7805885f7f98c557c26858bcb015b2860ac7fe14253732e + checksum: 10/a95488393abb0ba202bf5b92538bb89869a2cebb19ec596d179ae06f0ae169785b60797135b5f4ccddf7b52a40edb2ef0b88f8ef02b2ccacead0178520e3c9d7 languageName: node linkType: hard @@ -4271,26 +4271,26 @@ __metadata: languageName: unknown linkType: soft -"@openmrs/esm-framework@npm:5.6.1-pre.1955, @openmrs/esm-framework@npm:next": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-framework@npm:5.6.1-pre.1955" - dependencies: - "@openmrs/esm-api": "npm:5.6.1-pre.1955" - "@openmrs/esm-config": "npm:5.6.1-pre.1955" - "@openmrs/esm-context": "npm:5.6.1-pre.1955" - "@openmrs/esm-dynamic-loading": "npm:5.6.1-pre.1955" - "@openmrs/esm-error-handling": "npm:5.6.1-pre.1955" - "@openmrs/esm-extensions": "npm:5.6.1-pre.1955" - "@openmrs/esm-feature-flags": "npm:5.6.1-pre.1955" - "@openmrs/esm-globals": "npm:5.6.1-pre.1955" - "@openmrs/esm-navigation": "npm:5.6.1-pre.1955" - "@openmrs/esm-offline": "npm:5.6.1-pre.1955" - "@openmrs/esm-react-utils": "npm:5.6.1-pre.1955" - "@openmrs/esm-routes": "npm:5.6.1-pre.1955" - "@openmrs/esm-state": "npm:5.6.1-pre.1955" - "@openmrs/esm-styleguide": "npm:5.6.1-pre.1955" - "@openmrs/esm-translations": "npm:5.6.1-pre.1955" - "@openmrs/esm-utils": "npm:5.6.1-pre.1955" +"@openmrs/esm-framework@npm:5.6.1-pre.2033, @openmrs/esm-framework@npm:next": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-framework@npm:5.6.1-pre.2033" + dependencies: + "@openmrs/esm-api": "npm:5.6.1-pre.2033" + "@openmrs/esm-config": "npm:5.6.1-pre.2033" + "@openmrs/esm-context": "npm:5.6.1-pre.2033" + "@openmrs/esm-dynamic-loading": "npm:5.6.1-pre.2033" + "@openmrs/esm-error-handling": "npm:5.6.1-pre.2033" + "@openmrs/esm-extensions": "npm:5.6.1-pre.2033" + "@openmrs/esm-feature-flags": "npm:5.6.1-pre.2033" + "@openmrs/esm-globals": "npm:5.6.1-pre.2033" + "@openmrs/esm-navigation": "npm:5.6.1-pre.2033" + "@openmrs/esm-offline": "npm:5.6.1-pre.2033" + "@openmrs/esm-react-utils": "npm:5.6.1-pre.2033" + "@openmrs/esm-routes": "npm:5.6.1-pre.2033" + "@openmrs/esm-state": "npm:5.6.1-pre.2033" + "@openmrs/esm-styleguide": "npm:5.6.1-pre.2033" + "@openmrs/esm-translations": "npm:5.6.1-pre.2033" + "@openmrs/esm-utils": "npm:5.6.1-pre.2033" dayjs: "npm:^1.10.7" peerDependencies: dayjs: 1.x @@ -4301,7 +4301,7 @@ __metadata: rxjs: 6.x single-spa: 5.x swr: 2.x - checksum: 10/5313f9421c16099e0fa8d4b5031b5278c4cb2e0b43ad89dd57aa25fdd64402a72a6d9938fb3ba665ca3d8079213bac98a93553de9e80f198940d26cb6d94b745 + checksum: 10/d8328706d1e131dd1a3756bde77d9cd6bff33edc245c6127a8c0044d218b8401a9b9c7404cbc819b134e65626ddb1db7bb7c3cd52160ee4c26dffda7ff6f6313 languageName: node linkType: hard @@ -4327,31 +4327,31 @@ __metadata: languageName: unknown linkType: soft -"@openmrs/esm-globals@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-globals@npm:5.6.1-pre.1955" +"@openmrs/esm-globals@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-globals@npm:5.6.1-pre.2033" dependencies: "@types/fhir": "npm:0.0.31" peerDependencies: single-spa: 5.x - checksum: 10/4330f8feac4742bbae20a64aa201536d997c1436d09fa4e8a330b004a297d835d6d2899a57fa020a80cf547d73fff03265ab644472e8d63aa81ea9061db6267d + checksum: 10/af1f782ab0a4c55bbff0ba2f7f84ac2d2dad56dd51b6e7dcea5ab0f823f572d94350298aa75100a173d4833b007d808d98ed1b94ae50117647e812c029c5d6ee languageName: node linkType: hard -"@openmrs/esm-navigation@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-navigation@npm:5.6.1-pre.1955" +"@openmrs/esm-navigation@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-navigation@npm:5.6.1-pre.2033" dependencies: path-to-regexp: "npm:6.1.0" peerDependencies: "@openmrs/esm-state": 5.x - checksum: 10/3e9e5d472dcf59f49a5e5fe583e831e0d4960325ab3b3b5e8522dba20bcdf11f4e975cffa16e9ce9dd5e288a3a1306a77e2041253f181e21bfa4b8f251afd84e + checksum: 10/ea4b96648f24439229b02a48be9c23431a29675ebfcb2a174c6a9b29a1b33fe1da8e34ae209db4dedef781129de214ddf3a2bd116629c4dce9e33465f95818a6 languageName: node linkType: hard -"@openmrs/esm-offline@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-offline@npm:5.6.1-pre.1955" +"@openmrs/esm-offline@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-offline@npm:5.6.1-pre.2033" dependencies: dexie: "npm:^3.0.3" lodash-es: "npm:^4.17.21" @@ -4362,7 +4362,7 @@ __metadata: "@openmrs/esm-globals": 5.x "@openmrs/esm-state": 5.x rxjs: 6.x - checksum: 10/8df28ae0b9fff9345d0966bac5db489e906db729fce42df411181dd847a20ac4fb73cd3f67dedf5672e388585fa23d9c9d00283c899b3c089858a5acda3394e2 + checksum: 10/fdb0c3df5ed076e91021e9942d429c21a70cafce816e30639fd4a24aef9976f0279f3c97b78b2fb05346db75c44477c882737aea3a7d990bf358b4c64e8894c4 languageName: node linkType: hard @@ -4753,9 +4753,9 @@ __metadata: languageName: unknown linkType: soft -"@openmrs/esm-react-utils@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-react-utils@npm:5.6.1-pre.1955" +"@openmrs/esm-react-utils@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-react-utils@npm:5.6.1-pre.2033" dependencies: lodash-es: "npm:^4.17.21" single-spa-react: "npm:^6.0.0" @@ -4776,34 +4776,34 @@ __metadata: react-i18next: 11.x rxjs: 6.x swr: 2.x - checksum: 10/daa5a1800efa684a107ad4bab37ad531db3b88f38bf99cfab5b8548d34a82dfb03a209997c6f993073eed34d8417f96ac87d0e513ff7728acdedacea57a64c1d + checksum: 10/054d45d3e7d8a44b30c99ac6cc8935fc661118061466aa280a116389e873296a1a03504be4e81bdd28fab8bf6b9d53b9722e00715a95a803d8f625a43098a0ee languageName: node linkType: hard -"@openmrs/esm-routes@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-routes@npm:5.6.1-pre.1955" +"@openmrs/esm-routes@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-routes@npm:5.6.1-pre.2033" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-utils": 5.x - checksum: 10/ded4ad0e6638306c56be0fb865fd3d097e1b7ae5a763289979cb6e4e595df0b78d78adb32b8695f65d5246c88002ff9d9f7301579b642456b9374c9635520347 + checksum: 10/dcbaafd204a4a3a6ffaded053df28bb8b4a08897f9bc7216aaa5d76678c9f89ca34e4a03dfef6434c61bfead4ffdacec7c1aecba632eee5ff8c1989ebb302e16 languageName: node linkType: hard -"@openmrs/esm-state@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-state@npm:5.6.1-pre.1955" +"@openmrs/esm-state@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-state@npm:5.6.1-pre.2033" dependencies: zustand: "npm:^4.3.6" peerDependencies: "@openmrs/esm-globals": 5.x - checksum: 10/bae6809bc1de45e1a1a667a60b79a54edfc73d85791f46190e69151fcf55ae21f19792f2880845d819fd12a882127fff45c89a78eb2796d33875e2b95e436dc0 + checksum: 10/f431cad92ccb95a60626080d8beaeb7766cd30a9912e26bd3ef7b737f7766befe4a3521590fea4121f40ffdfa67466e3868ce18b32f652efb7bdc018805e3dc2 languageName: node linkType: hard -"@openmrs/esm-styleguide@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-styleguide@npm:5.6.1-pre.1955" +"@openmrs/esm-styleguide@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-styleguide@npm:5.6.1-pre.2033" dependencies: "@carbon/charts": "npm:^1.12.0" "@carbon/react": "npm:~1.37.0" @@ -4826,24 +4826,24 @@ __metadata: react: 18.x react-dom: 18.x rxjs: 6.x - checksum: 10/ca327a63960d1459f73d2f274e0e400f5079608cb41825132b027ceaa6cc98c411ed7e8bcf60f3bcafcacfc990aa469511ac312d08adf088290131a8628db59e + checksum: 10/be06dc6c38c2310db07809ec12a5a208044af1faab5bf903b3bcdffc18872f426f5ce7dbdf4df9b2c9e2cab448b994ed45a6de24c1ffe334857be868cc5a24bf languageName: node linkType: hard -"@openmrs/esm-translations@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-translations@npm:5.6.1-pre.1955" +"@openmrs/esm-translations@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-translations@npm:5.6.1-pre.2033" dependencies: i18next: "npm:21.10.0" peerDependencies: i18next: 21.x - checksum: 10/99dcb4c0737320a3d3e2d352b779f6b5403f8555be2fb6db4b68b5a434ee0d596cf6478736acdfc3ab968762b78578d215fb231411095f027d6458caaa277a44 + checksum: 10/2de216445098d32af648b95ab3bd580c088080e13222a5a7332df8a8b5acf0634e7fb8f0a19f8e7d051d37310b32a8615f432c5b1180a43b9bc63ece97feba83 languageName: node linkType: hard -"@openmrs/esm-utils@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/esm-utils@npm:5.6.1-pre.1955" +"@openmrs/esm-utils@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/esm-utils@npm:5.6.1-pre.2033" dependencies: "@internationalized/date": "npm:^3.5.4" semver: "npm:7.3.2" @@ -4852,7 +4852,7 @@ __metadata: dayjs: 1.x i18next: 21.x rxjs: 6.x - checksum: 10/fcbf236a0d32cb17162146857e5f19818a0d2d8fc6efd7b9752441ae7d11a05522e6d91970f92210b384289ad5d8f86122f7b41bc6a79fc4f20fd73e2a2ced9a + checksum: 10/620a9c215dcdf8f43c296f69d52f4117db2f1a51166fcc088fda8d1030d90b22636904eb676244a02f21e3b68dafbcb4fc0b9bfcd03c3b02a89efdd5ce6a5767 languageName: node linkType: hard @@ -4931,9 +4931,9 @@ __metadata: languageName: node linkType: hard -"@openmrs/webpack-config@npm:5.6.1-pre.1955": - version: 5.6.1-pre.1955 - resolution: "@openmrs/webpack-config@npm:5.6.1-pre.1955" +"@openmrs/webpack-config@npm:5.6.1-pre.2033": + version: 5.6.1-pre.2033 + resolution: "@openmrs/webpack-config@npm:5.6.1-pre.2033" dependencies: "@swc/core": "npm:^1.3.58" clean-webpack-plugin: "npm:^4.0.0" @@ -4950,7 +4950,7 @@ __metadata: webpack-stats-plugin: "npm:^1.0.3" peerDependencies: webpack: 5.x - checksum: 10/79b173f5532fb8bc38d32ec6955a5b43013d1be4cbd8065eb1e14df4cac99e930bc896c8e2f038313bbfa26c1398ec6bf6476099e47db2a226d8d28fba1e8d8b + checksum: 10/ddc4ee46e480f401aab3729e20046c5cdbdc405bd70d9923924a0d3a31ef3409bb010f1a950221905f74d74ce91ec4d4adddd123a87b2f172b71dace269a47e0 languageName: node linkType: hard @@ -18169,11 +18169,11 @@ __metadata: linkType: hard "openmrs@npm:next": - version: 5.6.1-pre.1955 - resolution: "openmrs@npm:5.6.1-pre.1955" + version: 5.6.1-pre.2033 + resolution: "openmrs@npm:5.6.1-pre.2033" dependencies: - "@openmrs/esm-app-shell": "npm:5.6.1-pre.1955" - "@openmrs/webpack-config": "npm:5.6.1-pre.1955" + "@openmrs/esm-app-shell": "npm:5.6.1-pre.2033" + "@openmrs/webpack-config": "npm:5.6.1-pre.2033" "@pnpm/npm-conf": "npm:^2.1.0" "@swc/core": "npm:^1.3.58" autoprefixer: "npm:^10.4.2" @@ -18205,7 +18205,7 @@ __metadata: yargs: "npm:^17.6.2" bin: openmrs: ./dist/cli.js - checksum: 10/044433e7077bf966343dc75da690369347acae88e601f399eb810dfa125be52f345b52403422ce1a0a6eccb6dbb973f19527a2f1bfe08f7a0064600bc298a921 + checksum: 10/3c904899288e1f22b6b7249081aea2ad0c886231dc1300002e8edb3b4c239eb9e04f18b34a05d6adb1d69b349dfefcd96bc1e6e6e801a60317991f514be7f1ed languageName: node linkType: hard From 8f946852035d2381d4bbf28d76795e734964aab6 Mon Sep 17 00:00:00 2001 From: Dennis Kigen Date: Fri, 5 Jul 2024 20:54:26 +0300 Subject: [PATCH 04/90] (test) Add testing-related plugins to ESLint config (#1898) * (test) Add testing-related plugins to ESLint config * Comment out flaky tests --- .eslintrc | 9 +++- package.json | 2 + .../src/actions-buttons/stop-visit.test.tsx | 4 +- .../chart-review/chart-review.test.tsx | 2 +- .../src/side-nav/side-menu.test.tsx | 2 +- .../src/visit/visit-form/visit-form.test.tsx | 2 +- .../visit-detail-overview.test.tsx | 2 +- .../src/conditions/conditions-form.test.tsx | 21 +++++---- .../src/forms/forms-list.test.tsx | 2 +- .../add-lab-order/add-lab-order.test.tsx | 12 ++--- .../active-medications.test.tsx | 4 +- .../add-drug-order/add-drug-order.test.tsx | 1 + .../order-basket-action-button.test.tsx | 3 +- .../src/programs/programs-form.test.tsx | 4 +- .../biometrics/biometrics-overview.test.tsx | 5 +- .../vitals-header.test.tsx | 2 +- .../vitals-biometrics-input.test.tsx | 4 +- .../src/vitals/vitals-overview.test.tsx | 2 +- yarn.lock | 47 ++++++++++++++++++- 19 files changed, 93 insertions(+), 37 deletions(-) diff --git a/.eslintrc b/.eslintrc index 49fb9c21fe..3cf458e060 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,9 +2,14 @@ "env": { "node": true }, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:jest-dom/recommended", + "plugin:testing-library/react" + ], "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint", "react-hooks"], + "plugins": ["@typescript-eslint", "jest-dom", "react-hooks", "testing-library"], "rules": { "react-hooks/exhaustive-deps": "warn", "react-hooks/rules-of-hooks": "error", diff --git a/package.json b/package.json index f86de1f3c9..2f2c6b223f 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,10 @@ "dayjs": "^1.11.10", "dotenv": "^16.3.1", "eslint": "^8.50.0", + "eslint-plugin-jest-dom": "^5.4.0", "eslint-plugin-playwright": "^0.16.0", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^6.2.2", "husky": "^8.0.3", "i18next": "^21.10.0", "i18next-parser": "^6.6.0", diff --git a/packages/esm-patient-chart-app/src/actions-buttons/stop-visit.test.tsx b/packages/esm-patient-chart-app/src/actions-buttons/stop-visit.test.tsx index f7f573333e..5c16ccf2e9 100644 --- a/packages/esm-patient-chart-app/src/actions-buttons/stop-visit.test.tsx +++ b/packages/esm-patient-chart-app/src/actions-buttons/stop-visit.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import StopVisitOverflowMenuItem from './stop-visit.component'; -import { screen, render, cleanup } from '@testing-library/react'; +import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { showModal, useVisit } from '@openmrs/esm-framework'; import { mockCurrentVisit } from '__mocks__'; @@ -18,8 +18,8 @@ jest.mock('@openmrs/esm-framework', () => ({ describe('StopVisitOverflowMenuItem', () => { beforeEach(() => { jest.clearAllMocks(); - cleanup(); }); + it('should be able to stop current visit', async () => { const user = userEvent.setup(); diff --git a/packages/esm-patient-chart-app/src/patient-chart/chart-review/chart-review.test.tsx b/packages/esm-patient-chart-app/src/patient-chart/chart-review/chart-review.test.tsx index cd4b1cd329..1f402df651 100644 --- a/packages/esm-patient-chart-app/src/patient-chart/chart-review/chart-review.test.tsx +++ b/packages/esm-patient-chart-app/src/patient-chart/chart-review/chart-review.test.tsx @@ -93,7 +93,7 @@ describe('ChartReview: ', () => { renderChartReview(); - expect(screen.getByRole('heading').textContent).toMatch(/Patient summary/i); + expect(screen.getByRole('heading')).toHaveTextContent(/Patient summary/i); }); }); diff --git a/packages/esm-patient-chart-app/src/side-nav/side-menu.test.tsx b/packages/esm-patient-chart-app/src/side-nav/side-menu.test.tsx index d9ec422d00..e19444fbcf 100644 --- a/packages/esm-patient-chart-app/src/side-nav/side-menu.test.tsx +++ b/packages/esm-patient-chart-app/src/side-nav/side-menu.test.tsx @@ -31,7 +31,7 @@ describe('sidemenu', () => { renderSideMenu(); - expect(screen.getByText(/left nav menu/)).toBeTruthy(); + expect(screen.getByText(/left nav menu/)).toBeInTheDocument(); }); it('is not rendered when viewport == tablet or viewport == small-desktop', () => { diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx index 2094acb091..8801989ece 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx @@ -541,7 +541,7 @@ describe('Visit Form', () => { expect(screen.getByText(/Part of the form did not load/i)).toBeInTheDocument(); expect(screen.getByText(/Please refresh to try again/i)).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Start visit/i })).not.toBeDisabled(); + expect(screen.getByRole('button', { name: /Start visit/i })).toBeEnabled(); }); it('should show an error if a required visit attribute type is not provided', async () => { diff --git a/packages/esm-patient-chart-app/src/visit/visits-widget/visit-detail-overview.test.tsx b/packages/esm-patient-chart-app/src/visit/visits-widget/visit-detail-overview.test.tsx index be97fac8ff..da6884c69a 100644 --- a/packages/esm-patient-chart-app/src/visit/visits-widget/visit-detail-overview.test.tsx +++ b/packages/esm-patient-chart-app/src/visit/visits-widget/visit-detail-overview.test.tsx @@ -112,7 +112,7 @@ describe('VisitDetailOverview', () => { expect(visitSummariesTab).toBeInTheDocument(); expect(visitSummariesTab).toHaveAttribute('aria-selected', 'true'); - expect(screen.queryByText('/All encounters/i')).toBeNull(); + expect(screen.queryByText('/All encounters/i')).not.toBeInTheDocument(); expect(screen.getByRole('tab', { name: /notes/i })).toBeInTheDocument(); expect(screen.getByRole('tab', { name: /tests/i })).toBeInTheDocument(); expect(screen.getByRole('tab', { name: /medications/i })).toBeInTheDocument(); diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx index 51d86e731e..220a3fff5b 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx @@ -72,7 +72,7 @@ describe('Conditions form', () => { const cancelButton = screen.getByRole('button', { name: /Cancel/i }); const submitButton = screen.getByRole('button', { name: /Save & close/i }); expect(cancelButton).toBeInTheDocument(); - expect(cancelButton).not.toBeDisabled(); + expect(cancelButton).toBeEnabled(); expect(submitButton).toBeInTheDocument(); }); @@ -120,7 +120,7 @@ describe('Conditions form', () => { expect(getByTextWithMarkup('No results for "Post-acute sequelae of COVID-19"')).toBeInTheDocument(); }); - it('renders a success toast notification upon successfully recording a condition', async () => { + it('renders a success notification upon successfully recording a condition', async () => { const user = userEvent.setup(); mockOpenmrsFetch.mockResolvedValue({ data: [] } as FetchResponse); @@ -138,7 +138,7 @@ describe('Conditions form', () => { const activeStatusInput = screen.getByRole('radio', { name: 'Active' }); const conditionSearchInput = screen.getByRole('searchbox', { name: /enter condition/i }); const onsetDateInput = screen.getByRole('textbox', { name: /onset date/i }); - expect(cancelButton).not.toBeDisabled(); + expect(cancelButton).toBeEnabled(); await user.type(conditionSearchInput, 'Headache'); await user.click(screen.getByRole('menuitem', { name: /headache/i })); @@ -146,12 +146,13 @@ describe('Conditions form', () => { await user.type(onsetDateInput, '2020-05-05'); await user.click(submitButton); - expect(mockShowSnackbar).toHaveBeenCalled(); - expect(mockShowSnackbar).toHaveBeenCalledWith({ - kind: 'success', - subtitle: 'It is now visible on the Conditions page', - title: 'Condition saved', - }); + // TODO: Figure out why the following assertions are flaky + // expect(mockShowSnackbar).toHaveBeenCalled(); + // expect(mockShowSnackbar).toHaveBeenCalledWith({ + // kind: 'success', + // subtitle: 'It is now visible on the Conditions page', + // title: 'Condition saved', + // }); }); it('renders an error notification if there was a problem recording a condition', async () => { @@ -177,7 +178,7 @@ describe('Conditions form', () => { await user.type(onsetDateInput, '2020-05-05'); await user.click(activeStatusInput); expect(activeStatusInput).toBeChecked(); - expect(submitButton).not.toBeDisabled(); + expect(submitButton).toBeEnabled(); await user.click(submitButton); }); diff --git a/packages/esm-patient-forms-app/src/forms/forms-list.test.tsx b/packages/esm-patient-forms-app/src/forms/forms-list.test.tsx index e151f6d25a..d7e5069ad3 100644 --- a/packages/esm-patient-forms-app/src/forms/forms-list.test.tsx +++ b/packages/esm-patient-forms-app/src/forms/forms-list.test.tsx @@ -29,7 +29,7 @@ it('renders a list of forms fetched from the server', async () => { expect(screen.getByRole('button', { name: /clear search input/i })).toBeInTheDocument(); expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByRole('cell', { name: /Laboratory Tests/i })).toBeInTheDocument(); - expect(screen.queryByText('PiusRocks')).toBeNull(); + expect(screen.queryByText('PiusRocks')).not.toBeInTheDocument(); const expectedColumnHeaders = [/form name \(A-Z\)/, /last completed/]; diff --git a/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx b/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx index 08e48d4f5e..0b1378ce1f 100644 --- a/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx +++ b/packages/esm-patient-labs-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, renderHook, screen, waitFor, within } from '@testing-library/react'; +import { render, renderHook, screen, waitFor } from '@testing-library/react'; import AddLabOrderWorkspace from './add-lab-order.workspace'; import userEvent from '@testing-library/user-event'; import { _resetOrderBasketStore } from '@openmrs/esm-patient-common-lib/src/orders/store'; @@ -58,7 +58,7 @@ function renderAddLabOrderWorkspace() { onWorkspaceClose(); }); const mockPromptBeforeClosing = jest.fn(); - const renderResult = render( + const view = render( , ); - return { mockCloseWorkspace, mockPromptBeforeClosing, mockCloseWorkspaceWithSavedChanges, ...renderResult }; + return { mockCloseWorkspace, mockPromptBeforeClosing, mockCloseWorkspaceWithSavedChanges, ...view }; } describe('AddLabOrder', () => { @@ -106,7 +106,7 @@ describe('AddLabOrder', () => { await user.type(screen.getByRole('searchbox'), 'cd4'); const cd4 = screen.getByText('CD4 COUNT'); expect(cd4).toBeInTheDocument(); - const cd4OrderButton = within(cd4.closest('div').parentElement).getByText('Order form'); + const cd4OrderButton = screen.getAllByRole('button', { name: 'Order form' })[1]; await user.click(cd4OrderButton); const testType = screen.getByRole('combobox', { name: 'Test type' }); @@ -155,7 +155,7 @@ describe('AddLabOrder', () => { await user.type(screen.getByRole('searchbox'), 'cd4'); const cd4 = screen.getByText('CD4 COUNT'); expect(cd4).toBeInTheDocument(); - const cd4OrderButton = within(cd4.closest('div').parentElement).getByText('Add to basket'); + const cd4OrderButton = screen.getAllByRole('button', { name: 'Add to basket' })[1]; await user.click(cd4OrderButton); expect(hookResult.current.orders).toEqual([ { ...createEmptyLabOrder(mockTestTypes[1], 'test-provider-uuid'), isOrderIncomplete: true }, @@ -196,6 +196,6 @@ describe('AddLabOrder', () => { }, }); renderAddLabOrderWorkspace(); - expect(screen.getAllByText(/Error/i)[0]).toBeInTheDocument(); + expect(screen.getByText(/Error/i)).toBeInTheDocument(); }); }); diff --git a/packages/esm-patient-medications-app/src/active-medications/active-medications.test.tsx b/packages/esm-patient-medications-app/src/active-medications/active-medications.test.tsx index 8a5a04b5e6..1e07ecfdc5 100644 --- a/packages/esm-patient-medications-app/src/active-medications/active-medications.test.tsx +++ b/packages/esm-patient-medications-app/src/active-medications/active-medications.test.tsx @@ -115,7 +115,7 @@ test('clicking the Record active medications link opens the order basket form', mockOpenmrsFetch.mockReturnValueOnce({ data: { results: [] } }); renderActiveMedications(); await waitForLoadingToFinish(); - const orderLink = await screen.getByText('Record active medications'); + const orderLink = screen.getByText('Record active medications'); fireEvent.click(orderLink); expect(mockLaunchWorkspace).toHaveBeenCalledWith('add-drug-order'); }); @@ -124,7 +124,7 @@ test('clicking the Add button opens the order basket form', async () => { mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockPatientDrugOrdersApiData } }); renderActiveMedications(); await waitForLoadingToFinish(); - const button = await screen.getByRole('button', { name: /Add/i }); + const button = screen.getByRole('button', { name: /Add/i }); fireEvent.click(button); expect(mockLaunchWorkspace).toHaveBeenCalledWith('add-drug-order'); }); diff --git a/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx b/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx index 4aa8e0b09e..36d0f66d08 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx +++ b/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx @@ -1,3 +1,4 @@ +/* eslint-disable testing-library/no-node-access */ import React from 'react'; import { screen, render, within, renderHook, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; diff --git a/packages/esm-patient-orders-app/src/order-basket-action-button/order-basket-action-button.test.tsx b/packages/esm-patient-orders-app/src/order-basket-action-button/order-basket-action-button.test.tsx index f4eea0ff58..88256eb537 100644 --- a/packages/esm-patient-orders-app/src/order-basket-action-button/order-basket-action-button.test.tsx +++ b/packages/esm-patient-orders-app/src/order-basket-action-button/order-basket-action-button.test.tsx @@ -116,7 +116,8 @@ describe('', () => { activeVisit: null, currentVisit: null, })); - const screen = render(); + + render(); const orderBasketButton = screen.getByRole('button', { name: /order basket/i }); expect(orderBasketButton).toBeInTheDocument(); diff --git a/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx b/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx index 328469dba4..61a1bc0a2c 100644 --- a/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx +++ b/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx @@ -107,7 +107,7 @@ describe('ProgramsForm', () => { await user.tab(); - expect(enrollButton).not.toBeDisabled(); + expect(enrollButton).toBeEnabled(); await user.click(enrollButton); @@ -163,7 +163,7 @@ describe('ProgramsForm', () => { await user.selectOptions(selectProgramInput, [oncologyScreeningProgramUuid]); await user.selectOptions(selectLocationInput, [inpatientWardUuid]); - expect(enrollButton).not.toBeDisabled(); + expect(enrollButton).toBeEnabled(); await user.click(enrollButton); diff --git a/packages/esm-patient-vitals-app/src/biometrics/biometrics-overview.test.tsx b/packages/esm-patient-vitals-app/src/biometrics/biometrics-overview.test.tsx index a80e1845a6..62bc73651b 100644 --- a/packages/esm-patient-vitals-app/src/biometrics/biometrics-overview.test.tsx +++ b/packages/esm-patient-vitals-app/src/biometrics/biometrics-overview.test.tsx @@ -1,3 +1,4 @@ +/* eslint-disable testing-library/no-node-access */ import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -67,7 +68,7 @@ describe('BiometricsOverview: ', () => { await waitForLoadingToFinish(); - expect(screen.findByRole('heading', { name: /biometrics/i })); + await screen.findByRole('heading', { name: /biometrics/i }); expect(screen.queryByRole('table')).not.toBeInTheDocument(); expect(screen.getByText(/Error 401: Unauthorized/i)).toBeInTheDocument(); expect( @@ -89,7 +90,7 @@ describe('BiometricsOverview: ', () => { await waitForLoadingToFinish(); await screen.findByRole('heading', { name: /biometrics/i }); - await screen.findByRole('table', { name: /biometrics/i }); + screen.getByRole('table', { name: /biometrics/i }); expect(screen.getByRole('tab', { name: /table view/i })).toBeInTheDocument(); expect(screen.getByRole('tab', { name: /chart view/i })).toBeInTheDocument(); expect(screen.getByRole('link', { name: /see all/i })).toBeInTheDocument(); diff --git a/packages/esm-patient-vitals-app/src/vitals-and-biometrics-header/vitals-header.test.tsx b/packages/esm-patient-vitals-app/src/vitals-and-biometrics-header/vitals-header.test.tsx index 301e6f8060..d9999c0119 100644 --- a/packages/esm-patient-vitals-app/src/vitals-and-biometrics-header/vitals-header.test.tsx +++ b/packages/esm-patient-vitals-app/src/vitals-and-biometrics-header/vitals-header.test.tsx @@ -148,7 +148,7 @@ describe('VitalsHeader: ', () => { await waitForLoadingToFinish(); - expect(screen.queryByTitle(/abnormal value/i)).toBeInTheDocument(); + expect(screen.getByTitle(/abnormal value/i)).toBeInTheDocument(); }); it('should launch Form Entry vitals and biometrics form', async () => { diff --git a/packages/esm-patient-vitals-app/src/vitals-biometrics-form/vitals-biometrics-input.test.tsx b/packages/esm-patient-vitals-app/src/vitals-biometrics-form/vitals-biometrics-input.test.tsx index 5023bf4d45..a4e0e4745f 100644 --- a/packages/esm-patient-vitals-app/src/vitals-biometrics-form/vitals-biometrics-input.test.tsx +++ b/packages/esm-patient-vitals-app/src/vitals-biometrics-form/vitals-biometrics-input.test.tsx @@ -160,7 +160,7 @@ describe('VitalsAndBiometricsInput', () => { expect(screen.getByTitle(/notes/i)).toBeInTheDocument(); }); - it('should validate the input based on the provided interpretation and reference range values', () => { + it('should validate the input based on the provided interpretation and reference range values', async () => { const config = useConfig(); testProps.fieldProperties = [ @@ -181,7 +181,7 @@ describe('VitalsAndBiometricsInput', () => { renderVitalsBiometricsInput(); - screen.findByRole('spinbutton'); + await screen.findByRole('spinbutton'); expect(screen.getByRole('spinbutton', { name: /heart rate/i })).toBeInTheDocument(); const abnormalValueFlag = screen.getByTitle(/abnormal value/i); diff --git a/packages/esm-patient-vitals-app/src/vitals/vitals-overview.test.tsx b/packages/esm-patient-vitals-app/src/vitals/vitals-overview.test.tsx index c6a00213e2..fa01959bf7 100644 --- a/packages/esm-patient-vitals-app/src/vitals/vitals-overview.test.tsx +++ b/packages/esm-patient-vitals-app/src/vitals/vitals-overview.test.tsx @@ -82,7 +82,7 @@ describe('VitalsOverview', () => { await waitForLoadingToFinish(); - expect(screen.findByRole('heading', { name: /vitals/i })); + await screen.findByRole('heading', { name: /vitals/i }); expect(screen.queryByRole('table')).not.toBeInTheDocument(); expect(screen.getByText(/Error 401: Unauthorized/i)).toBeInTheDocument(); expect( diff --git a/yarn.lock b/yarn.lock index 6c6bca3a50..e9c153020f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2014,6 +2014,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.16.3": + version: 7.24.7 + resolution: "@babel/runtime@npm:7.24.7" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/7b77f566165dee62db3db0296e71d08cafda3f34e1b0dcefcd68427272e17c1704f4e4369bff76651b07b6e49d3ea5a0ce344818af9116e9292e4381e0918c76 + languageName: node + linkType: hard + "@babel/template@npm:7.22.5": version: 7.22.5 resolution: "@babel/template@npm:7.22.5" @@ -4482,8 +4491,10 @@ __metadata: dayjs: "npm:^1.11.10" dotenv: "npm:^16.3.1" eslint: "npm:^8.50.0" + eslint-plugin-jest-dom: "npm:^5.4.0" eslint-plugin-playwright: "npm:^0.16.0" eslint-plugin-react-hooks: "npm:^4.6.0" + eslint-plugin-testing-library: "npm:^6.2.2" husky: "npm:^8.0.3" i18next: "npm:^21.10.0" i18next-parser: "npm:^6.6.0" @@ -7839,7 +7850,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.62.0": +"@typescript-eslint/utils@npm:5.62.0, @typescript-eslint/utils@npm:^5.58.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" dependencies: @@ -12383,6 +12394,22 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jest-dom@npm:^5.4.0": + version: 5.4.0 + resolution: "eslint-plugin-jest-dom@npm:5.4.0" + dependencies: + "@babel/runtime": "npm:^7.16.3" + requireindex: "npm:^1.2.0" + peerDependencies: + "@testing-library/dom": ^8.0.0 || ^9.0.0 || ^10.0.0 + eslint: ^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + peerDependenciesMeta: + "@testing-library/dom": + optional: true + checksum: 10/b8b0b0249d066658a75723892bc6f52d6bcf03ff0a69fc5020548c49f740613a8f3acce647f8f04b292606d2bd0ab3372a695aa3d90b4efb19e71870bbddf637 + languageName: node + linkType: hard + "eslint-plugin-playwright@npm:^0.16.0": version: 0.16.0 resolution: "eslint-plugin-playwright@npm:0.16.0" @@ -12405,6 +12432,17 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-testing-library@npm:^6.2.2": + version: 6.2.2 + resolution: "eslint-plugin-testing-library@npm:6.2.2" + dependencies: + "@typescript-eslint/utils": "npm:^5.58.0" + peerDependencies: + eslint: ^7.5.0 || ^8.0.0 + checksum: 10/61947d0b81de1565c8627ec2d1e6636a8b6613cfe554a4671d011b3e88dfd77b498ce83b15bcf0a2df5570c44ad1d46d54058ed488f4e515d764196cbc6d65cf + languageName: node + linkType: hard + "eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -20283,6 +20321,13 @@ __metadata: languageName: node linkType: hard +"requireindex@npm:^1.2.0": + version: 1.2.0 + resolution: "requireindex@npm:1.2.0" + checksum: 10/266d1cb31f6cbc4b6cf2e898f5bbc45581f7919bcf61bba5c45d0adb69b722b9ff5a13727be3350cde4520d7cd37f39df45d58a29854baaa4552cd6b05ae4a1a + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" From 9e466e87ec90a838773c9e555d676d82f7b5ee8e Mon Sep 17 00:00:00 2001 From: jnsereko <58003327+jnsereko@users.noreply.github.com> Date: Mon, 8 Jul 2024 10:04:13 +0300 Subject: [PATCH 05/90] (feat) O3-3552: Add feature flag for printing patient identifier stickers (#1893) (feat) Feature flag the ability to print identifier stickers Co-authored-by: Dennis Kigen --- .../src/config-schema.ts | 6 ----- .../print-identifier-sticker.component.tsx | 23 +++++++++---------- packages/esm-patient-chart-app/src/index.ts | 5 ++++ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/esm-patient-banner-app/src/config-schema.ts b/packages/esm-patient-banner-app/src/config-schema.ts index 18bab9c008..e283736134 100644 --- a/packages/esm-patient-banner-app/src/config-schema.ts +++ b/packages/esm-patient-banner-app/src/config-schema.ts @@ -33,11 +33,6 @@ export const configSchema = { 'Specifies the paper size for printing the sticker. You can define the size using units (e.g., mm, in) or named sizes (e.g., "148mm 210mm", "A1", "A2", "A4", "A5").', _default: '4in 6in', }, - showPrintIdentifierStickerButton: { - _type: Type.Boolean, - _description: "Whether to display the 'Print identifier sticker' button in the patient banner", - _default: false, - }, useRelationshipNameLink: { _type: Type.Boolean, _description: "Whether to use the relationship name as a link to the associated person's patient chart.", @@ -51,6 +46,5 @@ export interface ConfigObject { }; printIdentifierStickerFields: Array; printIdentifierStickerSize: string; - showPrintIdentifierStickerButton: boolean; useRelationshipNameLink: boolean; } diff --git a/packages/esm-patient-chart-app/src/actions-buttons/print-identifier-sticker.component.tsx b/packages/esm-patient-chart-app/src/actions-buttons/print-identifier-sticker.component.tsx index f56b8001fd..5201af1908 100644 --- a/packages/esm-patient-chart-app/src/actions-buttons/print-identifier-sticker.component.tsx +++ b/packages/esm-patient-chart-app/src/actions-buttons/print-identifier-sticker.component.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { OverflowMenuItem } from '@carbon/react'; -import { showModal, useConfig, usePatient } from '@openmrs/esm-framework'; +import { showModal, useFeatureFlag, usePatient } from '@openmrs/esm-framework'; import styles from './action-button.scss'; interface PrintIdentifierStickerOverflowMenuItemProps { @@ -13,9 +13,7 @@ const PrintIdentifierStickerOverflowMenuItem: React.FC { const { t } = useTranslation(); const { patient } = usePatient(patientUuid); - const { showPrintIdentifierStickerButton } = useConfig<{ showPrintIdentifierStickerButton: boolean }>({ - externalModuleName: '@openmrs/esm-patient-banner-app', - }); + const canPrintPatientIdentifierSticker = useFeatureFlag('print-patient-identifier-sticker'); const handleLaunchModal = useCallback(() => { const dispose = showModal('print-identifier-sticker-modal', { @@ -24,15 +22,16 @@ const PrintIdentifierStickerOverflowMenuItem: React.FC - ) + ); }; diff --git a/packages/esm-patient-chart-app/src/index.ts b/packages/esm-patient-chart-app/src/index.ts index c39b30bbd4..d2831af5af 100644 --- a/packages/esm-patient-chart-app/src/index.ts +++ b/packages/esm-patient-chart-app/src/index.ts @@ -50,6 +50,11 @@ export function startupApp() { 'Retrospective Data Entry', "Features to enter data for past visits. Includes the 'Edit Past Visit' button in the start visit dialog, and the 'Add Past Visit' button in the patient header.", ); + registerFeatureFlag( + 'print-patient-identifier-sticker', + 'Print patient identifier sticker', + 'Features to support printing a patient identifier sticker', + ); } export const root = getSyncLifecycle(patientChartPageComponent, { featureName: 'patient-chart', moduleName }); From cef1867f2c3a517a5a7e335c1cfc05c2f3aa000d Mon Sep 17 00:00:00 2001 From: Chima Nwadike Date: Mon, 8 Jul 2024 15:24:12 +0100 Subject: [PATCH 06/90] (test) O3-3361: E2E test that data isn't lost when opening a workspace from another workspace (#1886) --- e2e/specs/clinical-forms.spec.ts | 65 ++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/e2e/specs/clinical-forms.spec.ts b/e2e/specs/clinical-forms.spec.ts index 71348d6f1f..ec51756683 100644 --- a/e2e/specs/clinical-forms.spec.ts +++ b/e2e/specs/clinical-forms.spec.ts @@ -66,6 +66,25 @@ test('Fill a clinical form', async ({ page }) => { await page.getByLabel(/plan/i).fill(plan); }); + await test.step('And I click the `Order basket` button on the siderail', async () => { + await page.getByRole('button', { name: /order basket/i, exact: true }).click(); + }); + + await test.step('And I click the `Add +` button to order drugs', async () => { + await page.getByRole('button', { name: /add/i }).nth(1).click(); + }); + + await test.step('And I click the `Clinical forms` button on the siderail', async () => { + await page.getByLabel(/clinical forms/i, { exact: true }).click(); + }); + + await test.step('Then I should see retained inputs in `Soap note template` form', async () => { + await expect(page.getByText(subjectiveFindings)).toBeVisible(); + await expect(page.getByText(objectiveFindings)).toBeVisible(); + await expect(page.getByText(assessment)).toBeVisible(); + await expect(page.getByText(plan)).toBeVisible(); + }); + await test.step('And I click on the `Save and close` button', async () => { await page.getByRole('button', { name: /save/i }).click(); }); @@ -100,6 +119,52 @@ test('Fill a clinical form', async ({ page }) => { }); }); +test('Form state is retained when moving between forms in the workspace', async ({ page }) => { + const chartPage = new ChartPage(page); + + await test.step('When I visit the chart summary page', async () => { + await chartPage.goTo(patient.uuid); + }); + + await test.step('And I click the `Clinical forms` button on the siderail', async () => { + await page.getByLabel(/clinical forms/i, { exact: true }).click(); + }); + + await test.step('Then I should see `Soap note template` listed in the clinical forms workspace', async () => { + await expect(page.getByRole('cell', { name: /soap note template/i, exact: true })).toBeVisible(); + }); + + await test.step('When I click the `Soap note template` link to launch the form', async () => { + await page.getByText(/soap note template/i).click(); + }); + + await test.step('Then I should see the `Soap note template` form launch in the workspace', async () => { + await expect(page.getByText(/soap note template/i)).toBeVisible(); + }); + + await test.step('When I fill the `Subjective findings` and `Objective findings` questions', async () => { + await page.getByLabel(/subjective Findings/i).fill(subjectiveFindings); + await page.getByLabel(/objective findings/i).fill(objectiveFindings); + }); + + await test.step('And I click the `Order basket` button on the siderail', async () => { + await page.getByRole('button', { name: /order basket/i, exact: true }).click(); + }); + + await test.step('And I click the `Add +` button to order drugs', async () => { + await page.getByRole('button', { name: /add/i }).nth(1).click(); + }); + + await test.step('And I click the `Clinical forms` button on the siderail', async () => { + await page.getByLabel(/clinical forms/i, { exact: true }).click(); + }); + + await test.step('Then I should see retained inputs in `Soap note template` form', async () => { + await expect(page.getByText(subjectiveFindings)).toBeVisible(); + await expect(page.getByText(objectiveFindings)).toBeVisible(); + }); +}); + test.afterEach(async ({ api }) => { await endVisit(api, visit.uuid); await deletePatient(api, patient.uuid); From eec17974b490105b08a610bf1e6775906bc6e6d5 Mon Sep 17 00:00:00 2001 From: Dennis Kigen Date: Mon, 8 Jul 2024 19:06:38 +0300 Subject: [PATCH 07/90] (test) Some miscellaneous test fixes (#1902) * (test) Some miscellaneous test fixes * Change some translations to sentence case * Comment out flaky assertions * Undo schema mods --- __mocks__/mockDeceasedPatient.ts | 2 +- .../src/actions-buttons/cancel-visit.test.tsx | 9 +++----- .../translations/en.json | 22 +++++++++---------- .../src/empty-state/empty-state.component.tsx | 3 +-- .../src/conditions/conditions-form.test.tsx | 13 ++++++----- .../src/notes/visit-notes-form.workspace.tsx | 1 + .../translations/en.json | 2 +- tools/test-utils.tsx | 3 ++- 8 files changed, 27 insertions(+), 28 deletions(-) diff --git a/__mocks__/mockDeceasedPatient.ts b/__mocks__/mockDeceasedPatient.ts index d9b6eb3efa..3b4363b9de 100644 --- a/__mocks__/mockDeceasedPatient.ts +++ b/__mocks__/mockDeceasedPatient.ts @@ -19,7 +19,7 @@ export const mockDeceasedPatient = { value: '100732HE', }, { - id: '1f0ad7a1-430f-4397-b571-59ea654a52db', + id: 'ceda35a8-a445-455d-9928-ac088692190a', use: 'usual', system: 'OpenMRS ID', value: '100GEJ', diff --git a/packages/esm-patient-chart-app/src/actions-buttons/cancel-visit.test.tsx b/packages/esm-patient-chart-app/src/actions-buttons/cancel-visit.test.tsx index d2d12d4f9a..7f52559099 100644 --- a/packages/esm-patient-chart-app/src/actions-buttons/cancel-visit.test.tsx +++ b/packages/esm-patient-chart-app/src/actions-buttons/cancel-visit.test.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import CancelVisitOverflowMenuItem from './cancel-visit.component'; -import { screen, render, waitFor } from '@testing-library/react'; +import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { showModal, useVisit } from '@openmrs/esm-framework'; +import { useVisit } from '@openmrs/esm-framework'; import { mockCurrentVisit } from '__mocks__'; +import CancelVisitOverflowMenuItem from './cancel-visit.component'; const mockUseVisit = useVisit as jest.Mock; -const mockShowModal = showModal as jest.Mock; jest.mock('@openmrs/esm-framework', () => { const originalModule = jest.requireActual('@openmrs/esm-framework'); @@ -25,7 +24,5 @@ describe('CancelVisitOverflowMenuItem', () => { expect(cancelVisitButton).toBeInTheDocument(); await user.click(cancelVisitButton); - - expect(mockShowModal).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/esm-patient-chart-app/translations/en.json b/packages/esm-patient-chart-app/translations/en.json index 8de160d659..073a9f339b 100644 --- a/packages/esm-patient-chart-app/translations/en.json +++ b/packages/esm-patient-chart-app/translations/en.json @@ -19,13 +19,13 @@ "confirm": "Confirm", "confirmDeletingVisitTextWithStartAndEndDate": "Are you sure you want to delete {{visit}} which started {{visitStartDate}} and ended {{visitEndDate}}?", "confirmModifyingVisitDateToAccomodateEncounter": "The encounter date falls outside the designated visit date range. Would you like to modify the visit date to accommodate the new encounter date?", - "currentVisit": "Current Visit", + "currentVisit": "Current visit", "date": "Date", "dateAndTime": "Date & time", "dateOfDeath": "Date of death", "deathDateRequired": "Please select the date of death", "delete": "Delete", - "deleteEncounter": "Delete Encounter", + "deleteEncounter": "Delete encounter", "deleteEncounterConfirmationText": "Are you sure you want to delete this encounter? This action can't be undone.", "deleteThisEncounter": "Delete this encounter", "deleteVisit": "Delete visit", @@ -35,7 +35,7 @@ "diagnoses": "Diagnoses", "discard": "Discard", "dose": "Dose", - "editPastVisit": "Edit Past Visit", + "editPastVisit": "Edit past visit", "editThisEncounter": "Edit this encounter", "editThisVisit": "Edit this visit", "editVisitDetails": "Edit visit details", @@ -48,7 +48,7 @@ "end": "End", "endActiveVisitConfirmation": "Are you sure you want to end this active visit?", "endDate": "End date", - "endDate_title": "End Date", + "endDate_title": "End date", "endVisit": "End visit", "endVisit_title": "End Visit", "endVisitExplainerMessage": "Ending this visit means that you will no longer be able to add encounters to it. If you need to add an encounter, you can create a new visit for this patient or edit a past one.", @@ -116,9 +116,9 @@ "paginationPageText_one": "of {{count}} page", "paginationPageText_other": "of {{count}} pages", "partOfFormDidntLoad": "Part of the form did not load", - "pastVisitErrorText": "Past Visit Error", - "pastVisits": "Past Visits", - "Patient Summary": "Patient Summary", + "pastVisitErrorText": "Past visit error", + "pastVisits": "Past visits", + "Patient Summary": "Patient summary", "printIdentifierSticker": "Print identifier sticker", "program": "Program", "provider": "Provider", @@ -129,7 +129,7 @@ "record": "Record", "refills": "Refills", "refreshToTryAgain": "Please refresh to try again", - "retrospectiveEntry": "Retrospective Entry", + "retrospectiveEntry": "Retrospective entry", "saveAndClose": "Save and close", "saving": "Saving", "searchForAVisitType": "Search for a visit type", @@ -139,7 +139,7 @@ "selectAnOption": "Select an option", "selectLocation": "Select a location", "selectProgramType": "Select program type", - "selectVisitType": "Please select a Visit Type", + "selectVisitType": "Please select a visit type", "start": "Start", "startAVisit": "Start a visit", "startDate": "Start date", @@ -158,7 +158,7 @@ "updateVisit": "Update visit", "updatingVisit": "Updating visit", "visit": "Visit", - "visitAttributes": "Visit Attributes", + "visitAttributes": "Visit attributes", "visitCancelled": "Visit cancelled", "visitCancelSuccessMessage": "Active {{visit}} cancelled successfully", "visitDeleted": "{{visit}} deleted", @@ -167,7 +167,7 @@ "visitDetailsUpdatedSuccessfully": "{{visit}} updated successfully", "visitEnded": "Visit ended", "visitEndSuccessfully": "Ended current visit successfully", - "visitLocation": "Visit Location", + "visitLocation": "Visit location", "visitNotRestored": "Visit couldn't be restored", "visitRestored": "Visit restored", "visitRestoredSuccessfully": "{{visit}} restored successfully", diff --git a/packages/esm-patient-common-lib/src/empty-state/empty-state.component.tsx b/packages/esm-patient-common-lib/src/empty-state/empty-state.component.tsx index 6ca323fbc7..637eccc589 100644 --- a/packages/esm-patient-common-lib/src/empty-state/empty-state.component.tsx +++ b/packages/esm-patient-common-lib/src/empty-state/empty-state.component.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { Layer, Link, Tile } from '@carbon/react'; +import { Button, Layer, Tile } from '@carbon/react'; import { useTranslation } from 'react-i18next'; import { EmptyDataIllustration } from './empty-data-illustration.component'; import { useLayoutType } from '@openmrs/esm-framework'; import styles from './empty-state.scss'; -import { Button } from '@carbon/react'; export interface EmptyStateProps { displayText: string; diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx index 220a3fff5b..109b17f681 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx @@ -215,12 +215,13 @@ describe('Conditions form', () => { expect(screen.queryByText(/a condition is required/i)).not.toBeInTheDocument(); expect(screen.queryByText(/a clinical status is required/i)).not.toBeInTheDocument(); - expect(mockShowSnackbar).toHaveBeenCalled(); - expect(mockShowSnackbar).toHaveBeenCalledWith({ - kind: 'success', - subtitle: 'It is now visible on the Conditions page', - title: 'Condition saved', - }); + // TODO: Figure out why the following assertions are flaky + // expect(mockShowSnackbar).toHaveBeenCalled(); + // expect(mockShowSnackbar).toHaveBeenCalledWith({ + // kind: 'success', + // subtitle: 'It is now visible on the Conditions page', + // title: 'Condition saved', + // }); }); it('launching the form with an existing condition prepopulates the form with the condition details', async () => { diff --git a/packages/esm-patient-notes-app/src/notes/visit-notes-form.workspace.tsx b/packages/esm-patient-notes-app/src/notes/visit-notes-form.workspace.tsx index 3c03329508..91c3534775 100644 --- a/packages/esm-patient-notes-app/src/notes/visit-notes-form.workspace.tsx +++ b/packages/esm-patient-notes-app/src/notes/visit-notes-form.workspace.tsx @@ -129,6 +129,7 @@ const VisitNotesForm: React.FC = ({ mode: 'onSubmit', resolver: zodResolver(visitNoteFormSchema), defaultValues: { + primaryDiagnosisSearch: '', noteDate: new Date(), }, }); diff --git a/packages/esm-patient-orders-app/translations/en.json b/packages/esm-patient-orders-app/translations/en.json index b3e10b5536..bf2e51e550 100644 --- a/packages/esm-patient-orders-app/translations/en.json +++ b/packages/esm-patient-orders-app/translations/en.json @@ -6,7 +6,7 @@ "cancel": "Cancel", "cancellationDate": "Cancellation date", "cancellationDateRequired": "Cancellation date is required", - "cancelOrder": "Cancel Order", + "cancelOrder": "Cancel order", "dateCannotBeBeforeToday": "Date cannot be before today", "dateOfOrder": "Date of Order", "discard": "Discard", diff --git a/tools/test-utils.tsx b/tools/test-utils.tsx index 43e4fdaf6d..7502b64c11 100644 --- a/tools/test-utils.tsx +++ b/tools/test-utils.tsx @@ -1,3 +1,4 @@ +/* eslint-disable testing-library/no-node-access */ import React from 'react'; import { SWRConfig } from 'swr'; import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'; @@ -65,7 +66,7 @@ export const mockPatient = { value: '100732HE', }, { - id: '1f0ad7a1-430f-4397-b571-59ea654a52db', + id: 'ceda35a8-a445-455d-9928-ac088692190a', use: 'usual', system: 'OpenMRS ID', type: { text: 'OpenMRS ID', coding: [{ code: 'OpenMRS ID' }] }, From 7c820e3ae169ed5625974daa763510ea89632b74 Mon Sep 17 00:00:00 2001 From: Ian <52504170+ibacher@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:25:09 -0400 Subject: [PATCH 08/90] (fix) Allow cancelling orders and address various small fixes (#1899) --- .../src/orders/types.ts | 4 +- .../src/components/order-details-table.scss | 57 ++++- .../orders-details-table.component.tsx | 179 ++++++++------- .../src/utils/{utils.ts => index.ts} | 37 +--- .../translations/en.json | 6 +- tsconfig.json | 7 +- yarn.lock | 206 +++++++++--------- 7 files changed, 263 insertions(+), 233 deletions(-) rename packages/esm-patient-orders-app/src/utils/{utils.ts => index.ts} (78%) diff --git a/packages/esm-patient-common-lib/src/orders/types.ts b/packages/esm-patient-common-lib/src/orders/types.ts index e37f00ab9a..470dd5d80b 100644 --- a/packages/esm-patient-common-lib/src/orders/types.ts +++ b/packages/esm-patient-common-lib/src/orders/types.ts @@ -124,7 +124,7 @@ export interface Order { quantityUnits: OpenmrsResource; route: OpenmrsResource; scheduleDate: null; - urgency: string; + urgency: 'ROUTINE' | 'STAT' | 'ON_SCHEDULED_DATE'; // additional properties accessionNumber: string; @@ -139,7 +139,7 @@ export interface Order { changedBy: string; dateChanged: string; }; - fulfillerStatus: string; + fulfillerStatus: 'RECEIVED' | 'IN_PROGRESS' | 'EXCEPTION' | 'ON_HOLD' | 'DECLINED' | 'COMPLETED' | 'DISCONINTUED'; fulfillerComment: string; specimenSource: string; laterality: string; diff --git a/packages/esm-patient-orders-app/src/components/order-details-table.scss b/packages/esm-patient-orders-app/src/components/order-details-table.scss index 4825856ed9..e9437c89f1 100644 --- a/packages/esm-patient-orders-app/src/components/order-details-table.scss +++ b/packages/esm-patient-orders-app/src/components/order-details-table.scss @@ -1,7 +1,7 @@ @use '@carbon/styles/scss/colors'; @use '@carbon/styles/scss/spacing'; @use '@carbon/styles/scss/type'; -@import "../root"; +@use '@openmrs/esm-styleguide/src/vars' as openmrs; .widgetCard { border: 1px solid colors.$gray-20; @@ -30,19 +30,66 @@ justify-content: flex-end; } -.singleLineText { +.priorityPill { + @include type.type-style('label-01'); + background-color: colors.$gray-20; + border-radius: spacing.$spacing-05; + padding: spacing.$spacing-01; + text-align: center; + + &[data-urgency='routine'] { + background-color: colors.$green-20; + } + + &[data-urgency='urgent'] { + background-color: colors.$red-20; + } +} + +.statusPill { + @include type.type-style('label-01'); + background-color: colors.$gray-20; + border-radius: spacing.$spacing-05; + padding: spacing.$spacing-01; + text-align: center; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + + &[data-status='received'] { + background-color: colors.$blue-20; + } + + &[data-status='in progress'] { + background-color: colors.$cyan-20; + } + + &[data-status='on hold'] { + background-color: colors.$teal-20; + } + + &[data-status='exception'] { + background-color: colors.$magenta-20; + } + + &[data-status='completed'] { + background-color: colors.$green-20; + } + + &[data-status='discontinued'], + &[data-status='declined'] { + background-color: colors.$red-20; + } } .tile { text-align: center; - border-bottom: 1px solid $ui-03; + border-bottom: 1px solid openmrs.$ui-03; .message { - @include type.type-style("heading-compact-01"); - color: $text-02; + @include type.type-style('heading-compact-01'); + color: openmrs.$text-02; } } diff --git a/packages/esm-patient-orders-app/src/components/orders-details-table.component.tsx b/packages/esm-patient-orders-app/src/components/orders-details-table.component.tsx index f5fa3537c3..367497fff1 100644 --- a/packages/esm-patient-orders-app/src/components/orders-details-table.component.tsx +++ b/packages/esm-patient-orders-app/src/components/orders-details-table.component.tsx @@ -1,6 +1,5 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import capitalize from 'lodash-es/capitalize'; -import orderBy from 'lodash-es/orderBy'; +import React, { type ReactElement, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { capitalize, lowerCase } from 'lodash-es'; import { useTranslation } from 'react-i18next'; import { useReactToPrint } from 'react-to-print'; import { @@ -21,7 +20,6 @@ import { TableHead, TableHeader, TableRow, - Tag, Tooltip, } from '@carbon/react'; import { @@ -41,17 +39,19 @@ import { getDrugOrderByUuid, launchPatientWorkspace, } from '@openmrs/esm-patient-common-lib'; -import { Add, Printer } from '@carbon/react/icons'; import { age, + getCoreTranslation, getPatientName, formatDate, useConfig, useLayoutType, usePagination, usePatient, + PrinterIcon, + AddIcon, } from '@openmrs/esm-framework'; -import { buildLabOrder, buildMedicationOrder, compare, orderPriorityToColor, orderStatusColor } from '../utils/utils'; +import { buildLabOrder, buildMedicationOrder } from '../utils'; import MedicationRecord from './medication-record.component'; import PrintComponent from '../print/print.component'; import TestOrder from './test-order.component'; @@ -88,10 +88,10 @@ const OrderDetailsTable: React.FC = ({ title, patientUuid, sh const patient = usePatient(patientUuid); const { excludePatientIdentifierCodeTypes } = useConfig(); const [isPrinting, setIsPrinting] = useState(false); - const [sortParams, setSortParams] = useState({ key: '', order: 'none' }); const { orders, setOrders } = useOrderBasket(); const { data: orderTypes } = useOrderTypes(); const [selectedOrderTypeUuid, setSelectedOrderTypeUuid] = useState(null); + const selectedOrderName = orderTypes?.find((x) => x.uuid === selectedOrderTypeUuid)?.name; const { data: allOrders, @@ -102,7 +102,7 @@ const OrderDetailsTable: React.FC = ({ title, patientUuid, sh // launch respective order basket based on order type const openOrderForm = useCallback( - (orderItem) => { + (orderItem: Order) => { switch (orderItem.type) { case 'drugorder': launchAddDrugOrder(); @@ -163,20 +163,35 @@ const OrderDetailsTable: React.FC = ({ title, patientUuid, sh const tableRows = useMemo(() => { return allOrders?.map((order) => ({ id: order.uuid, + dateActivated: order.dateActivated, orderNumber: order.orderNumber, dateOfOrder:
{formatDate(new Date(order.dateActivated))}
, orderType: capitalize(order.orderType?.display ?? '--'), order: order.display, priority: ( -
- {capitalize(order.urgency)} +
+ { + // t('ON_SCHEDULED_DATE', 'On scheduled date') + // t('ROUTINE', 'Routine') + // t('STAT', 'STAT') + } + {t(order.urgency, capitalize(order.urgency.replace('_', ' ')))}
), orderedBy: order.orderer?.display, status: order.fulfillerStatus ? ( - - {order.fulfillerStatus} - +
+ { + // t('RECEIVED', 'Received') + // t('IN_PROGRESS', 'In progress') + // t('EXCEPTION', 'Exception') + // t('ON_HOLD', 'On hold') + // t('DECLINED', 'Declined') + // t('COMPLETED', 'Completed') + // t('DISCONTINUED', 'Discontinued') + } + {t(order.fulfillerStatus, capitalize(order.fulfillerStatus.replace('_', ' ')))} +
) : ( '--' ), @@ -190,40 +205,21 @@ const OrderDetailsTable: React.FC = ({ title, patientUuid, sh /> ), })); - }, [allOrders, isPrinting, launchOrderBasket, orders, setOrders, openOrderForm]); - - const sortRow = (cellA, cellB, { sortDirection, sortStates }) => { - return sortDirection === sortStates.DESC - ? compare(cellB.sortKey, cellA.sortKey) - : compare(cellA.sortKey, cellB.sortKey); - }; + }, [allOrders, isPrinting, launchOrderBasket, orders, setOrders, openOrderForm, t]); - const sortDate = (myArray, order) => - order === 'ASC' - ? orderBy(myArray, [(obj) => new Date(obj.encounterDate).getTime()], ['desc']) - : orderBy(myArray, [(obj) => new Date(obj.encounterDate).getTime()], ['asc']); - - const { key, order } = sortParams; - const sortedData = - key === 'dateOfOrder' - ? sortDate(tableRows, order) - : order === 'DESC' - ? orderBy(tableRows, [key], ['desc']) - : orderBy(tableRows, [key], ['asc']); - - const { results: paginatedOrders, goTo, currentPage } = usePagination(sortedData, defaultPageSize); + const { results: paginatedOrders, goTo, currentPage } = usePagination(tableRows, defaultPageSize); const patientDetails = useMemo(() => { const getGender = (gender: string): string => { switch (gender) { case 'male': - return t('male', 'Male'); + return getCoreTranslation('male'); case 'female': - return t('female', 'Female'); + return getCoreTranslation('female'); case 'other': - return t('other', 'Other'); + return getCoreTranslation('other'); case 'unknown': - return t('unknown', 'Unknown'); + return getCoreTranslation('unknown'); default: return gender; } @@ -241,7 +237,7 @@ const OrderDetailsTable: React.FC = ({ title, patientUuid, sh location: patient?.patient?.address?.[0].city, identifiers: identifiers?.length ? identifiers.map(({ value, type }) => value) : [], }; - }, [patient, t, excludePatientIdentifierCodeTypes?.uuids]); + }, [patient, excludePatientIdentifierCodeTypes?.uuids]); const onBeforeGetContentResolve = useRef(null); @@ -286,7 +282,7 @@ const OrderDetailsTable: React.FC = ({ title, patientUuid, sh items={[...[{ display: 'All' }], ...orderTypes]} selectedItem={orderTypes.find((x) => x.uuid === selectedOrderTypeUuid)} itemToString={(orderType: OrderType) => (orderType ? capitalize(orderType.display) : '')} - onChange={(e) => { + onChange={(e: { selectedItem: Order }) => { if (e.selectedItem.display === 'All') { setSelectedOrderTypeUuid(null); return; @@ -301,7 +297,12 @@ const OrderDetailsTable: React.FC = ({ title, patientUuid, sh displayText={ selectedOrderTypeUuid === null ? t('orders', 'Orders') - : (orderTypes?.find((x) => x.uuid === selectedOrderTypeUuid)).display + 's' + : // t('Drug Order_few', 'Drug Orders') + // t('Test Order_few', 'Test Orders') + t(selectedOrderName, { + count: 3, + default: selectedOrderName, + }) } launchForm={launchOrderBasket} /> @@ -317,7 +318,7 @@ const OrderDetailsTable: React.FC = ({ title, patientUuid, sh {showPrintButton && ( )} - {availablePrograms?.length && eligiblePrograms?.length === 0 ? ( + {isEnrolledInAllPrograms && ( - ) : null} + )} {({ rows, headers, getHeaderProps, getTableProps }) => ( @@ -142,7 +149,7 @@ const ProgramsDetailedSummary: React.FC = ({ patie {cell.value?.content ?? cell.value} ))} - + ))} @@ -157,10 +164,9 @@ const ProgramsDetailedSummary: React.FC = ({ patie return ; }; -function ProgramEditButton({ programEnrollmentId }: ProgramEditButtonProps) { +function ProgramEditButton({ programEnrollmentId, t }: ProgramEditButtonProps) { const isTablet = useLayoutType() === 'tablet'; - const { t } = useTranslation(); - const launchEditProgramsForm = React.useCallback( + const launchEditProgramsForm = useCallback( () => launchPatientWorkspace('programs-form-workspace', { programEnrollmentId }), [programEnrollmentId], ); diff --git a/packages/esm-patient-programs-app/src/programs/programs-detailed-summary.test.tsx b/packages/esm-patient-programs-app/src/programs/programs-detailed-summary.test.tsx index 6ecda40442..5a4e275c75 100644 --- a/packages/esm-patient-programs-app/src/programs/programs-detailed-summary.test.tsx +++ b/packages/esm-patient-programs-app/src/programs/programs-detailed-summary.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { screen, within } from '@testing-library/react'; import { openmrsFetch } from '@openmrs/esm-framework'; import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; -import { mockEnrolledProgramsResponse } from '__mocks__'; +import { mockCareProgramsResponse, mockEnrolledInAllProgramsResponse, mockEnrolledProgramsResponse } from '__mocks__'; import { mockPatient, renderWithSwr, waitForLoadingToFinish } from 'tools'; import ProgramsDetailedSummary from './programs-detailed-summary.component'; @@ -27,7 +27,7 @@ jest.mock('@openmrs/esm-patient-common-lib', () => { }; }); -describe('ProgramsDetailedSummary ', () => { +describe('ProgramsDetailedSummary', () => { it('renders an empty state view when the patient is not enrolled into any programs', async () => { mockOpenmrsFetch.mockReturnValueOnce({ data: { results: [] } }); @@ -99,6 +99,21 @@ describe('ProgramsDetailedSummary ', () => { programEnrollmentId: mockEnrolledProgramsResponse[0].uuid, }); }); + + it('renders a notification when the patient is enrolled in all available programs', async () => { + mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockEnrolledInAllProgramsResponse } }); + mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockCareProgramsResponse } }); + + renderProgramsOverview(); + + await waitForLoadingToFinish(); + + expect(screen.getByRole('row', { name: /hiv care and treatment/i })).toBeInTheDocument(); + expect(screen.getByRole('row', { name: /hiv differentiated care/i })).toBeInTheDocument(); + expect(screen.getByRole('row', { name: /oncology screening and diagnosis/i })).toBeInTheDocument(); + expect(screen.getByText(/enrolled in all programs/i)).toBeInTheDocument(); + expect(screen.getByText(/there are no more programs left to enroll this patient in/i)).toBeInTheDocument(); + }); }); function renderProgramsOverview() { diff --git a/packages/esm-patient-programs-app/src/programs/programs-form.scss b/packages/esm-patient-programs-app/src/programs/programs-form.scss index 3b50fc40e5..458c06576e 100644 --- a/packages/esm-patient-programs-app/src/programs/programs-form.scss +++ b/packages/esm-patient-programs-app/src/programs/programs-form.scss @@ -14,6 +14,10 @@ align-content: flex-start; align-items: baseline; min-width: 50%; + + :global(.cds--inline-loading__text) { + @include type.type-style('body-01'); + } } .tablet { diff --git a/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx b/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx index 61a1bc0a2c..f95ff4ef7f 100644 --- a/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx +++ b/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx @@ -1,51 +1,42 @@ import React from 'react'; -import { throwError } from 'rxjs'; -import { of } from 'rxjs/internal/observable/of'; -import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { createErrorHandler, openmrsFetch, showSnackbar } from '@openmrs/esm-framework'; +import { render, screen } from '@testing-library/react'; +import { openmrsFetch, showSnackbar } from '@openmrs/esm-framework'; +import { mockPatient } from 'tools'; import { mockCareProgramsResponse, mockEnrolledProgramsResponse, mockLocationsResponse } from '__mocks__'; import { createProgramEnrollment, updateProgramEnrollment } from './programs.resource'; -import { mockPatient } from 'tools'; import ProgramsForm from './programs-form.workspace'; -const testProps = { - closeWorkspace: jest.fn(), - closeWorkspaceWithSavedChanges: jest.fn(), - patientUuid: mockPatient.id, - promptBeforeClosing: jest.fn(), - setTitle: jest.fn(), -}; - -const mockCreateErrorHandler = createErrorHandler as jest.Mock; const mockCreateProgramEnrollment = createProgramEnrollment as jest.Mock; const mockUpdateProgramEnrollment = updateProgramEnrollment as jest.Mock; const mockOpenmrsFetch = openmrsFetch as jest.Mock; const mockShowSnackbar = showSnackbar as jest.Mock; - -jest.mock('@openmrs/esm-framework', () => { - const originalModule = jest.requireActual('@openmrs/esm-framework'); - - return { - ...originalModule, - createErrorHandler: jest.fn(), - showSnackbar: jest.fn(), - useLocations: jest.fn().mockImplementation(() => mockLocationsResponse), - }; -}); - -jest.mock('./programs.resource', () => { - const originalModule = jest.requireActual('./programs.resource'); - - return { - ...originalModule, - createProgramEnrollment: jest.fn(), - updateProgramEnrollment: jest.fn(), - }; -}); +const mockCloseWorkspaceWithSavedChanges = jest.fn(); + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + showSnackbar: jest.fn(), + useLocations: jest.fn().mockImplementation(() => mockLocationsResponse), +})); + +jest.mock('./programs.resource', () => ({ + ...jest.requireActual('./programs.resource'), + createProgramEnrollment: jest.fn(), + updateProgramEnrollment: jest.fn(), + useEnrollments: jest.fn().mockReturnValue({ + data: mockEnrolledProgramsResponse, + isLoading: false, + isError: false, + mutateEnrollments: jest.fn().mockResolvedValue(undefined), + }), +})); describe('ProgramsForm', () => { - xit('renders a success toast notification upon successfully recording a program enrollment', async () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders a success toast notification upon successfully recording a program enrollment', async () => { const user = userEvent.setup(); const inpatientWardUuid = 'b1a8b05e-3542-4037-bbd3-998ee9c40574'; @@ -53,18 +44,21 @@ describe('ProgramsForm', () => { mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockCareProgramsResponse } }); mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockEnrolledProgramsResponse } }); - mockCreateProgramEnrollment.mockReturnValueOnce(of({ status: 201, statusText: 'Created' })); + mockCreateProgramEnrollment.mockResolvedValueOnce({ status: 201, statusText: 'Created' }); renderProgramsForm(); + const programNameInput = screen.getByRole('combobox', { name: /program name/i }); + const enrollmentDateInput = screen.getByRole('textbox', { name: /date enrolled/i }); + const enrollmentLocationInput = screen.getByRole('combobox', { name: /enrollment location/i }); const enrollButton = screen.getByRole('button', { name: /save and close/i }); - const enrollmentDateInput = screen.getAllByRole('textbox', { name: '' })[0]; - const selectLocationInput = screen.getAllByRole('combobox', { name: '' })[1]; - const selectProgramInput = screen.getAllByRole('combobox', { name: '' })[0]; + + await user.click(enrollButton); + expect(screen.getByText(/programrequired/i)).toBeInTheDocument(); await user.type(enrollmentDateInput, '2020-05-05'); - await user.selectOptions(selectProgramInput, [oncologyScreeningProgramUuid]); - await user.selectOptions(selectLocationInput, [inpatientWardUuid]); + await user.selectOptions(programNameInput, [oncologyScreeningProgramUuid]); + await user.selectOptions(enrollmentLocationInput, [inpatientWardUuid]); expect(screen.getByRole('option', { name: /Inpatient Ward/i })).toBeInTheDocument(); @@ -81,34 +75,27 @@ describe('ProgramsForm', () => { new AbortController(), ); + expect(mockCloseWorkspaceWithSavedChanges).toHaveBeenCalledTimes(1); expect(mockShowSnackbar).toHaveBeenCalledTimes(1); expect(mockShowSnackbar).toHaveBeenCalledWith({ - isLowContrast: true, subtitle: 'It is now visible in the Programs table', kind: 'success', title: 'Program enrollment saved', }); }); - xit('updates a program enrollment', async () => { + it('updates a program enrollment', async () => { const user = userEvent.setup(); renderProgramsForm(mockEnrolledProgramsResponse[0].uuid); const enrollButton = screen.getByRole('button', { name: /save and close/i }); - const dateCompletedGroup = screen.getByRole('group', { name: /Date completed/i }); - const dateCompletedInput = within(dateCompletedGroup).getByRole('textbox'); - - mockUpdateProgramEnrollment.mockReturnValueOnce(of({ status: 200, statusText: 'OK' })); - - await user.type(dateCompletedInput, '05/05/2020'); + const completionDateInput = screen.getByRole('textbox', { name: /date completed/i }); - expect(dateCompletedInput).toHaveValue('05/05/2020'); + mockUpdateProgramEnrollment.mockResolvedValueOnce({ status: 200, statusText: 'OK' }); + await user.type(completionDateInput, '05/05/2020'); await user.tab(); - - expect(enrollButton).toBeEnabled(); - await user.click(enrollButton); expect(mockUpdateProgramEnrollment).toHaveBeenCalledTimes(1); @@ -126,7 +113,6 @@ describe('ProgramsForm', () => { expect(mockShowSnackbar).toHaveBeenCalledWith( expect.objectContaining({ - isLowContrast: true, subtitle: 'Changes to the program are now visible in the Programs table', kind: 'success', title: 'Program enrollment updated', @@ -134,7 +120,7 @@ describe('ProgramsForm', () => { ); }); - xit('renders an error notification if there was a problem recording a program enrollment', async () => { + it('renders an error notification if there was a problem recording a program enrollment', async () => { const user = userEvent.setup(); const inpatientWardUuid = 'b1a8b05e-3542-4037-bbd3-998ee9c40574'; @@ -150,27 +136,25 @@ describe('ProgramsForm', () => { mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockCareProgramsResponse } }); mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockEnrolledProgramsResponse } }); - mockCreateProgramEnrollment.mockReturnValueOnce(throwError(error)); + mockCreateProgramEnrollment.mockRejectedValueOnce(error); renderProgramsForm(); + const programNameInput = screen.getByRole('combobox', { name: /program name/i }); + const enrollmentDateInput = screen.getByRole('textbox', { name: /date enrolled/i }); + const enrollmentLocationInput = screen.getByRole('combobox', { name: /enrollment location/i }); const enrollButton = screen.getByRole('button', { name: /save and close/i }); - const enrollmentDateInput = screen.getAllByRole('textbox', { name: '' })[0]; - const selectLocationInput = screen.getAllByRole('combobox', { name: '' })[1]; - const selectProgramInput = screen.getAllByRole('combobox', { name: '' })[0]; await user.type(enrollmentDateInput, '2020-05-05'); - await user.selectOptions(selectProgramInput, [oncologyScreeningProgramUuid]); - await user.selectOptions(selectLocationInput, [inpatientWardUuid]); + await user.selectOptions(programNameInput, [oncologyScreeningProgramUuid]); + await user.selectOptions(enrollmentLocationInput, [inpatientWardUuid]); expect(enrollButton).toBeEnabled(); await user.click(enrollButton); - expect(mockCreateErrorHandler).toHaveBeenCalledTimes(1); expect(mockShowSnackbar).toHaveBeenCalledWith({ - isLowContrast: false, - subtitle: 'Internal Server Error', + subtitle: 'An unknown error occurred', kind: 'error', title: 'Error saving program enrollment', }); @@ -178,5 +162,13 @@ describe('ProgramsForm', () => { }); function renderProgramsForm(programEnrollmentUuidToEdit?: string) { + const testProps = { + closeWorkspace: jest.fn(), + closeWorkspaceWithSavedChanges: mockCloseWorkspaceWithSavedChanges, + patientUuid: mockPatient.id, + promptBeforeClosing: jest.fn(), + setTitle: jest.fn(), + }; + render(); } diff --git a/packages/esm-patient-programs-app/src/programs/programs-form.workspace.tsx b/packages/esm-patient-programs-app/src/programs/programs-form.workspace.tsx index 22f6e2afb7..b5db1e2aa8 100644 --- a/packages/esm-patient-programs-app/src/programs/programs-form.workspace.tsx +++ b/packages/esm-patient-programs-app/src/programs/programs-form.workspace.tsx @@ -1,9 +1,7 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import filter from 'lodash-es/filter'; -import includes from 'lodash-es/includes'; -import map from 'lodash-es/map'; import { Button, ButtonSet, @@ -11,6 +9,7 @@ import { DatePickerInput, Form, FormGroup, + InlineLoading, InlineNotification, Layer, Select, @@ -21,12 +20,12 @@ import { z } from 'zod'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { - createErrorHandler, + parseDate, showSnackbar, - useSession, - useLocations, + translateFrom, useLayoutType, - parseDate, + useLocations, + useSession, } from '@openmrs/esm-framework'; import { type DefaultPatientWorkspaceProps } from '@openmrs/esm-patient-common-lib'; import { @@ -35,6 +34,7 @@ import { useEnrollments, updateProgramEnrollment, } from './programs.resource'; +import { moduleName } from '../dashboard.meta'; import styles from './programs-form.scss'; interface ProgramsFormProps extends DefaultPatientWorkspaceProps { @@ -42,7 +42,9 @@ interface ProgramsFormProps extends DefaultPatientWorkspaceProps { } const programsFormSchema = z.object({ - selectedProgram: z.string().refine((value) => value != '', 'Please select a valid program'), + selectedProgram: z + .string() + .refine((value) => !!value, translateFrom(moduleName, 'programRequired', 'Program is required')), enrollmentDate: z.date(), completionDate: z.date().nullable(), enrollmentLocation: z.string(), @@ -63,8 +65,9 @@ const ProgramsForm: React.FC = ({ const availableLocations = useLocations(); const { data: availablePrograms } = useAvailablePrograms(); const { data: enrollments, mutateEnrollments } = useEnrollments(patientUuid); + const [isSubmittingForm, setIsSubmittingForm] = useState(false); - const currentEnrollment = programEnrollmentId && enrollments.filter((e) => e.uuid == programEnrollmentId)[0]; + const currentEnrollment = programEnrollmentId && enrollments.filter((e) => e.uuid === programEnrollmentId)[0]; const currentProgram = currentEnrollment ? { display: currentEnrollment.program.name, @@ -74,7 +77,11 @@ const ProgramsForm: React.FC = ({ const eligiblePrograms = currentProgram ? [currentProgram] - : filter(availablePrograms, (program) => !includes(map(enrollments, 'program.uuid'), program.uuid)); + : filter(availablePrograms, (program) => { + const existingEnrollment = enrollments.find((e) => e.program.uuid === program.uuid); + + return !existingEnrollment || existingEnrollment.dateCompleted !== null; + }); const getLocationUuid = () => { if (!currentEnrollment?.location.uuid && session?.sessionLocation?.uuid) { @@ -103,8 +110,8 @@ const ProgramsForm: React.FC = ({ promptBeforeClosing(() => isDirty); }, [isDirty, promptBeforeClosing]); - const onSubmit = React.useCallback( - (data: ProgramsFormData) => { + const onSubmit = useCallback( + async (data: ProgramsFormData) => { const { selectedProgram, enrollmentDate, completionDate, enrollmentLocation } = data; const payload = { @@ -115,66 +122,38 @@ const ProgramsForm: React.FC = ({ location: enrollmentLocation, }; - const abortController = new AbortController(); - const sub = currentEnrollment - ? updateProgramEnrollment(currentEnrollment.uuid, payload, abortController).subscribe( - (response) => { - if (response.status === 200) { - mutateEnrollments(); - closeWorkspaceWithSavedChanges(); + try { + const abortController = new AbortController(); - showSnackbar({ - isLowContrast: true, - kind: 'success', - subtitle: t( - 'enrollmentUpdatesNowVisible', - 'Changes to the program are now visible in the Programs table', - ), - title: t('enrollmentUpdated', 'Program enrollment updated'), - }); - } - }, - (err) => { - createErrorHandler(); + if (currentEnrollment) { + await updateProgramEnrollment(currentEnrollment.uuid, payload, abortController); + } else { + await createProgramEnrollment(payload, abortController); + } - showSnackbar({ - title: t('programEnrollmentSaveError', 'Error saving program enrollment'), - kind: 'error', - isLowContrast: false, - subtitle: err?.message, - }); - }, - ) - : createProgramEnrollment(payload, abortController).subscribe( - (response) => { - if (response.status === 201) { - mutateEnrollments(); - closeWorkspaceWithSavedChanges(); + await mutateEnrollments(); + closeWorkspaceWithSavedChanges(); - showSnackbar({ - isLowContrast: true, - kind: 'success', - subtitle: t('enrollmentNowVisible', 'It is now visible in the Programs table'), - title: t('enrollmentSaved', 'Program enrollment saved'), - }); - } - }, - (err) => { - createErrorHandler(); + showSnackbar({ + kind: 'success', + title: currentEnrollment + ? t('enrollmentUpdated', 'Program enrollment updated') + : t('enrollmentSaved', 'Program enrollment saved'), + subtitle: currentEnrollment + ? t('enrollmentUpdatesNowVisible', 'Changes to the program are now visible in the Programs table') + : t('enrollmentNowVisible', 'It is now visible in the Programs table'), + }); + } catch (error) { + showSnackbar({ + kind: 'error', + title: t('programEnrollmentSaveError', 'Error saving program enrollment'), + subtitle: error instanceof Error ? error.message : 'An unknown error occurred', + }); + } - showSnackbar({ - title: t('programEnrollmentSaveError', 'Error saving program enrollment'), - kind: 'error', - isLowContrast: false, - subtitle: err?.message, - }); - }, - ); - return () => { - sub.unsubscribe(); - }; + setIsSubmittingForm(false); }, - [patientUuid, currentEnrollment, mutateEnrollments, closeWorkspaceWithSavedChanges, t], + [closeWorkspaceWithSavedChanges, currentEnrollment, mutateEnrollments, patientUuid, t], ); const programSelect = ( @@ -187,7 +166,7 @@ const ProgramsForm: React.FC = ({ aria-label="program name" id="program" invalidText={t('required', 'Required')} - labelText="" + labelText={t('programName', 'Program name')} onChange={(event) => onChange(event.target.value)} value={value} > @@ -220,7 +199,7 @@ const ProgramsForm: React.FC = ({ onChange={([date]) => onChange(date)} value={value} > - + )} /> @@ -242,7 +221,7 @@ const ProgramsForm: React.FC = ({ onChange={([date]) => onChange(date)} value={value} > - + )} /> @@ -256,8 +235,7 @@ const ProgramsForm: React.FC = ({ onChange(event.target.value)} value={value} diff --git a/packages/esm-patient-programs-app/src/programs/programs.resource.tsx b/packages/esm-patient-programs-app/src/programs/programs.resource.tsx index 1a59b3c431..b918c2eb4e 100644 --- a/packages/esm-patient-programs-app/src/programs/programs.resource.tsx +++ b/packages/esm-patient-programs-app/src/programs/programs.resource.tsx @@ -24,7 +24,7 @@ export function useEnrollments(patientUuid: string) { return { data: data ? uniqBy(formattedEnrollments, (program) => program?.program?.uuid) : null, - isError: error, + error, isLoading, isValidating, activeEnrollments, @@ -47,7 +47,7 @@ export function useAvailablePrograms(enrollments?: Array) { return { data: availablePrograms, - isError: error, + error, isLoading, eligiblePrograms, }; @@ -72,7 +72,7 @@ export function updateProgramEnrollment(programEnrollmentUuid: string, payload, if (!payload && !payload.program) { return null; } - const { program, dateEnrolled, dateCompleted, location } = payload; + const { dateEnrolled, dateCompleted, location } = payload; return openmrsFetch(`${restBaseUrl}/programenrollment/${programEnrollmentUuid}`, { method: 'POST', headers: { @@ -86,7 +86,7 @@ export function updateProgramEnrollment(programEnrollmentUuid: string, payload, export const usePrograms = (patientUuid: string) => { const { data: enrollments, - isError: enrollError, + error: enrollError, isLoading: enrolLoading, isValidating, activeEnrollments, diff --git a/packages/esm-patient-programs-app/src/types/index.ts b/packages/esm-patient-programs-app/src/types/index.ts index 89010cb605..2e0832167a 100644 --- a/packages/esm-patient-programs-app/src/types/index.ts +++ b/packages/esm-patient-programs-app/src/types/index.ts @@ -15,11 +15,11 @@ export interface PatientProgram { states: Array<{}>; links?: Links; }>; - concept: { + concept?: { display: string; uuid: string; }; - links: Links; + links?: Links; }; display: string; dateEnrolled: string; @@ -27,12 +27,12 @@ export interface PatientProgram { location?: { uuid: string; display: string; - links: Links; + links?: Links; }; voided?: boolean; outcome?: null; states?: []; - links: Links; + links?: Links; resourceVersion?: string; } diff --git a/packages/esm-patient-programs-app/translations/en.json b/packages/esm-patient-programs-app/translations/en.json index a23a802045..e37efaec13 100644 --- a/packages/esm-patient-programs-app/translations/en.json +++ b/packages/esm-patient-programs-app/translations/en.json @@ -25,9 +25,9 @@ "programEnrollmentSaveError": "Error saving program enrollment", "programEnrollmentWorkspaceTitle": "Record program enrollment", "programName": "Program name", + "programRequired": "Program is required", "programs": "Program enrollments", "Programs": "Programs", - "required": "Required", "saveAndClose": "Save and close", "saving": "Saving", "seeAll": "See all", From 8d99e3cac90c396721d2e2f739155d3000532240 Mon Sep 17 00:00:00 2001 From: Dennis Kigen Date: Fri, 12 Jul 2024 16:20:19 +0300 Subject: [PATCH 17/90] (test) Fix yet more flakiness in ConditionsForm tests --- .../src/conditions/conditions-form.test.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx index 946a0094f3..967d40df31 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import dayjs from 'dayjs'; import userEvent from '@testing-library/user-event'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { type FetchResponse, openmrsFetch, showSnackbar } from '@openmrs/esm-framework'; import { mockFhirConditionsResponse, searchedCondition } from '__mocks__'; import { getByTextWithMarkup, mockPatient } from 'tools'; @@ -146,7 +146,9 @@ describe('Conditions form', () => { await user.type(onsetDateInput, '2020-05-05'); await user.click(submitButton); - expect(mockShowSnackbar).toHaveBeenCalled(); + await waitFor(() => { + expect(mockShowSnackbar).toHaveBeenCalled(); + }); expect(mockShowSnackbar).toHaveBeenCalledWith({ kind: 'success', subtitle: 'It is now visible on the Conditions page', @@ -223,7 +225,9 @@ describe('Conditions form', () => { expect(screen.queryByText(/a condition is required/i)).not.toBeInTheDocument(); expect(screen.queryByText(/a clinical status is required/i)).not.toBeInTheDocument(); - expect(mockShowSnackbar).toHaveBeenCalled(); + await waitFor(() => { + expect(mockShowSnackbar).toHaveBeenCalled(); + }); expect(mockShowSnackbar).toHaveBeenCalledWith({ kind: 'success', subtitle: 'It is now visible on the Conditions page', From 4b7bba837bd9331ca4e4d37626a0de5ab0798534 Mon Sep 17 00:00:00 2001 From: Samuel Male Date: Fri, 12 Jul 2024 18:52:28 +0300 Subject: [PATCH 18/90] (chore) Bump React Form Engine library (#1913) (chore) Bump RFE lib --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8e5f5543fb..6180eb5da3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4915,8 +4915,8 @@ __metadata: linkType: hard "@openmrs/openmrs-form-engine-lib@npm:next": - version: 2.0.0-pre.1260 - resolution: "@openmrs/openmrs-form-engine-lib@npm:2.0.0-pre.1260" + version: 2.1.0-pre.1312 + resolution: "@openmrs/openmrs-form-engine-lib@npm:2.1.0-pre.1312" dependencies: "@carbon/react": "npm:>1.47.0 <1.50.0" ace-builds: "npm:^1.33.2" @@ -4938,7 +4938,7 @@ __metadata: react-i18next: 11.x rxjs: 6.x swr: 2.x - checksum: 10/3266f4c4df6254f46017808dc5fec6071c8ab3a7476147285d182cee1679156f037ef058cdea18f55fce1175007c4dbd6997742eaa695419dd555d6d1efa077e + checksum: 10/11d37d5d3014ca88f0c50c81d97c346e12eae7b54fb69bbc839b5eac31f7df96f4a7f669469ba8fda1451b0b0cd1bf3bbe5aac0a7782bd1b5630ddb2382694c8 languageName: node linkType: hard From 2c15335d5eaff2beb282406ba68a70ddaf8076b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 13 Jul 2024 00:02:49 +0300 Subject: [PATCH 19/90] (chore) Update translations from Transifex (#1906) Co-authored-by: OpenMRS Bot --- packages/esm-patient-programs-app/translations/am.json | 4 +++- packages/esm-patient-programs-app/translations/ar.json | 4 +++- packages/esm-patient-programs-app/translations/es.json | 4 +++- packages/esm-patient-programs-app/translations/fr.json | 4 +++- packages/esm-patient-programs-app/translations/he.json | 4 +++- packages/esm-patient-programs-app/translations/km.json | 4 +++- packages/esm-patient-programs-app/translations/zh.json | 4 +++- packages/esm-patient-programs-app/translations/zh_CN.json | 4 +++- 8 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/esm-patient-programs-app/translations/am.json b/packages/esm-patient-programs-app/translations/am.json index 5a5ea2da76..e37efaec13 100644 --- a/packages/esm-patient-programs-app/translations/am.json +++ b/packages/esm-patient-programs-app/translations/am.json @@ -3,6 +3,7 @@ "active": "Active", "activePrograms": "Active programs", "add": "Add", + "addPrograms": "Add programs", "cancel": "Cancel", "carePrograms": "Care Programs", "chooseProgram": "Choose a program", @@ -24,10 +25,11 @@ "programEnrollmentSaveError": "Error saving program enrollment", "programEnrollmentWorkspaceTitle": "Record program enrollment", "programName": "Program name", + "programRequired": "Program is required", "programs": "Program enrollments", "Programs": "Programs", - "required": "Required", "saveAndClose": "Save and close", + "saving": "Saving", "seeAll": "See all", "status": "Status" } diff --git a/packages/esm-patient-programs-app/translations/ar.json b/packages/esm-patient-programs-app/translations/ar.json index c2c138c587..e44a37a929 100644 --- a/packages/esm-patient-programs-app/translations/ar.json +++ b/packages/esm-patient-programs-app/translations/ar.json @@ -3,6 +3,7 @@ "active": "نشط", "activePrograms": "البرامج النشطة", "add": "أضف", + "addPrograms": "Add programs", "cancel": "إلغاء", "carePrograms": "برامج الرعاية", "chooseProgram": "اختر برنامجًا", @@ -24,10 +25,11 @@ "programEnrollmentSaveError": "خطأ في حفظ التسجيل في البرنامج", "programEnrollmentWorkspaceTitle": "Record program enrollment", "programName": "اسم البرنامج", + "programRequired": "Program is required", "programs": "تسجيلات البرنامج", "Programs": "البرامج", - "required": "مطلوب", "saveAndClose": "حفظ وإغلاق", + "saving": "Saving", "seeAll": "عرض الكل", "status": "الحالة" } diff --git a/packages/esm-patient-programs-app/translations/es.json b/packages/esm-patient-programs-app/translations/es.json index 3dd4e60f2a..87cce3f9d5 100644 --- a/packages/esm-patient-programs-app/translations/es.json +++ b/packages/esm-patient-programs-app/translations/es.json @@ -3,6 +3,7 @@ "active": "Activo", "activePrograms": "Programas activos", "add": "Añadir", + "addPrograms": "Add programs", "cancel": "Cancelar", "carePrograms": "Programas de atención", "chooseProgram": "Elegir un programa", @@ -24,10 +25,11 @@ "programEnrollmentSaveError": "Error al guardar inscripción en programa", "programEnrollmentWorkspaceTitle": "Record program enrollment", "programName": "Nombre de programa", + "programRequired": "Program is required", "programs": "Programas", "Programs": "Programas", - "required": "Requerido", "saveAndClose": "Guardar y cerrar", + "saving": "Saving", "seeAll": "Ver todos", "status": "Estado" } diff --git a/packages/esm-patient-programs-app/translations/fr.json b/packages/esm-patient-programs-app/translations/fr.json index cbc757aca0..063a3227bb 100644 --- a/packages/esm-patient-programs-app/translations/fr.json +++ b/packages/esm-patient-programs-app/translations/fr.json @@ -3,6 +3,7 @@ "active": "Actif", "activePrograms": "Programmes actifs", "add": "Ajouter", + "addPrograms": "Add programs", "cancel": "Annuller", "carePrograms": "Programmes de soins", "chooseProgram": "Choisissez un programme", @@ -24,10 +25,11 @@ "programEnrollmentSaveError": "Erreur d'enregistrement dans le programme", "programEnrollmentWorkspaceTitle": "Record program enrollment", "programName": "Nom du programme", + "programRequired": "Program is required", "programs": "programmes", "Programs": "Programs", - "required": "Requis", "saveAndClose": "Sauvegarder et fermer", + "saving": "Saving", "seeAll": "Voir tout", "status": "Statut" } diff --git a/packages/esm-patient-programs-app/translations/he.json b/packages/esm-patient-programs-app/translations/he.json index b8e12c6563..c83b4703a3 100644 --- a/packages/esm-patient-programs-app/translations/he.json +++ b/packages/esm-patient-programs-app/translations/he.json @@ -3,6 +3,7 @@ "active": "פעיל", "activePrograms": "תוכניות פעילות", "add": "הוסף", + "addPrograms": "Add programs", "cancel": "בטל", "carePrograms": "תוכניות טיפול", "chooseProgram": "בחר תוכנית", @@ -24,10 +25,11 @@ "programEnrollmentSaveError": "שגיאה בשמירת הרשמת התוכנית", "programEnrollmentWorkspaceTitle": "Record program enrollment", "programName": "שם התוכנית", + "programRequired": "Program is required", "programs": "תוכניות", "Programs": "תוכניות", - "required": "נדרש", "saveAndClose": "שמור וסגור", + "saving": "Saving", "seeAll": "ראה הכל", "status": "סטטוס" } diff --git a/packages/esm-patient-programs-app/translations/km.json b/packages/esm-patient-programs-app/translations/km.json index 4bfde488cd..d435906d6f 100644 --- a/packages/esm-patient-programs-app/translations/km.json +++ b/packages/esm-patient-programs-app/translations/km.json @@ -3,6 +3,7 @@ "active": "សកម្ម", "activePrograms": "កម្មវិធីសកម្ម", "add": "បន្ថែម", + "addPrograms": "Add programs", "cancel": "បោះបង់", "carePrograms": "កម្មវិធីថែទាំ", "chooseProgram": "ជ្រើសរើសកម្មវិធីមួយ", @@ -24,10 +25,11 @@ "programEnrollmentSaveError": "កំហុសក្នុងការរក្សាទុកការចុះឈ្មោះកម្មវិធី", "programEnrollmentWorkspaceTitle": "Record program enrollment", "programName": "ឈ្មោះកម្មវិធី", + "programRequired": "Program is required", "programs": "កម្មវិធី", "Programs": "កម្មវិធី", - "required": "តម្រូវការ", "saveAndClose": "រក្សាទុក និងបិទ", + "saving": "Saving", "seeAll": "ឃើញទាំងអស់", "status": "ស្ថានភាព" } diff --git a/packages/esm-patient-programs-app/translations/zh.json b/packages/esm-patient-programs-app/translations/zh.json index 2860b903e2..6b94a73087 100644 --- a/packages/esm-patient-programs-app/translations/zh.json +++ b/packages/esm-patient-programs-app/translations/zh.json @@ -3,6 +3,7 @@ "active": "活跃", "activePrograms": "活跃项目", "add": "添加", + "addPrograms": "Add programs", "cancel": "取消", "carePrograms": "护理项目", "chooseProgram": "选择一个项目", @@ -24,10 +25,11 @@ "programEnrollmentSaveError": "保存项目登记时出现错误", "programEnrollmentWorkspaceTitle": "记录项目登记", "programName": "项目名称", + "programRequired": "Program is required", "programs": "项目登记", "Programs": "项目", - "required": "必填", "saveAndClose": "保存并关闭", + "saving": "Saving", "seeAll": "查看全部", "status": "状态" } diff --git a/packages/esm-patient-programs-app/translations/zh_CN.json b/packages/esm-patient-programs-app/translations/zh_CN.json index 2860b903e2..6b94a73087 100644 --- a/packages/esm-patient-programs-app/translations/zh_CN.json +++ b/packages/esm-patient-programs-app/translations/zh_CN.json @@ -3,6 +3,7 @@ "active": "活跃", "activePrograms": "活跃项目", "add": "添加", + "addPrograms": "Add programs", "cancel": "取消", "carePrograms": "护理项目", "chooseProgram": "选择一个项目", @@ -24,10 +25,11 @@ "programEnrollmentSaveError": "保存项目登记时出现错误", "programEnrollmentWorkspaceTitle": "记录项目登记", "programName": "项目名称", + "programRequired": "Program is required", "programs": "项目登记", "Programs": "项目", - "required": "必填", "saveAndClose": "保存并关闭", + "saving": "Saving", "seeAll": "查看全部", "status": "状态" } From 496f608df2d2d98129d47b05d83af4917b879d52 Mon Sep 17 00:00:00 2001 From: McCarthy <121826239+mccarthyaaron@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:39:23 +0300 Subject: [PATCH 20/90] (fix) O3-2995: Allow empty quantity to dispense and prescription refills in Drug Order Form (#1754) Co-authored-by: Dennis Kigen --- e2e/specs/drug-orders.spec.ts | 10 + .../add-drug-order/add-drug-order.test.tsx | 19 +- .../drug-order-form.component.tsx | 244 ++++++++++-------- .../src/add-drug-order/drug-order-form.scss | 1 + .../src/api/api.ts | 32 ++- .../translations/en.json | 1 + 6 files changed, 194 insertions(+), 113 deletions(-) diff --git a/e2e/specs/drug-orders.spec.ts b/e2e/specs/drug-orders.spec.ts index 146a426a82..299d72d3e7 100644 --- a/e2e/specs/drug-orders.spec.ts +++ b/e2e/specs/drug-orders.spec.ts @@ -72,6 +72,16 @@ test('Record, edit and discontinue a drug order', async ({ page }) => { await form.getByLabel(/^duration$/i).fill('3'); }); + await test.step('And I set the quantity to dispense to 3', async () => { + await form.getByLabel(/^quantity to dispense$/i).clear(); + await form.getByLabel(/^quantity to dispense$/i).fill('3'); + }); + + await test.step('And I set the prescription refills to 1', async () => { + await form.getByLabel(/^prescription refills$/i).clear(); + await form.getByLabel(/^prescription refills$/i).fill('1'); + }); + await test.step('And I set the indication to `Headache`', async () => { await form.getByLabel(/indication/i).clear(); await form.getByLabel(/indication/i).fill('Headache'); diff --git a/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx b/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx index 36d0f66d08..0eddfb0a83 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx +++ b/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.test.tsx @@ -1,26 +1,27 @@ /* eslint-disable testing-library/no-node-access */ import React from 'react'; -import { screen, render, within, renderHook, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { screen, render, within, renderHook, waitFor } from '@testing-library/react'; import { getByTextWithMarkup } from 'tools'; import { getTemplateOrderBasketItem, useDrugSearch, useDrugTemplate } from './drug-search/drug-search.resource'; -import AddDrugOrderWorkspace from './add-drug-order.workspace'; import { mockDrugSearchResultApiData, mockDrugOrderTemplateApiData, mockPatientDrugOrdersApiData } from '__mocks__'; -import { type PostDataPrepFunction, useOrderBasket } from '@openmrs/esm-patient-common-lib'; import { closeWorkspace, useSession } from '@openmrs/esm-framework'; +import { type PostDataPrepFunction, useOrderBasket } from '@openmrs/esm-patient-common-lib'; import { _resetOrderBasketStore } from '@openmrs/esm-patient-common-lib/src/orders/store'; +import AddDrugOrderWorkspace from './add-drug-order.workspace'; const mockCloseWorkspace = closeWorkspace as jest.Mock; -mockCloseWorkspace.mockImplementation((name, { onWorkspaceClose }) => onWorkspaceClose()); - +const mockLaunchPatientWorkspace = jest.fn(); const mockUseSession = useSession as jest.Mock; +const usePatientOrdersMock = jest.fn(); + +mockCloseWorkspace.mockImplementation((name, { onWorkspaceClose }) => onWorkspaceClose()); mockUseSession.mockReturnValue({ currentProvider: { uuid: 'mock-provider-uuid', }, }); -const mockLaunchPatientWorkspace = jest.fn(); jest.mock('@openmrs/esm-patient-common-lib', () => ({ ...jest.requireActual('@openmrs/esm-patient-common-lib'), launchPatientWorkspace: (...args) => mockLaunchPatientWorkspace(...args), @@ -42,10 +43,12 @@ jest.mock('./drug-search/drug-search.resource', () => ({ useDebounce: jest.fn().mockImplementation((x) => x), })); -const usePatientOrdersMock = jest.fn(); jest.mock('../api/api', () => ({ ...jest.requireActual('../api/api'), usePatientOrders: () => usePatientOrdersMock(), + useRequireOutpatientQuantity: jest + .fn() + .mockReturnValue({ requireOutpatientQuantity: false, error: null, isLoading: false }), })); function renderDrugSearch() { @@ -166,7 +169,7 @@ describe('AddDrugOrderWorkspace drug search', () => { const indicationField = screen.getByRole('textbox', { name: 'Indication' }); await user.type(indicationField, 'Hypertension'); const saveFormButton = screen.getByText(/Save order/i); - fireEvent.click(saveFormButton); + await user.click(saveFormButton); await waitFor(() => expect(hookResult.current.orders).toEqual([ diff --git a/packages/esm-patient-medications-app/src/add-drug-order/drug-order-form.component.tsx b/packages/esm-patient-medications-app/src/add-drug-order/drug-order-form.component.tsx index 405e57907b..d25035ea79 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/drug-order-form.component.tsx +++ b/packages/esm-patient-medications-app/src/add-drug-order/drug-order-form.component.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { type TFunction, useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { useTranslation } from 'react-i18next'; import capitalize from 'lodash-es/capitalize'; import { Button, @@ -11,8 +11,8 @@ import { DatePicker, DatePickerInput, Form, - FormLabel, FormGroup, + FormLabel, Grid, InlineNotification, Layer, @@ -26,12 +26,11 @@ import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { type Control, Controller, useController, useForm } from 'react-hook-form'; import { - ExtensionSlot, age, - getPatientName, + ExtensionSlot, formatDate, + getPatientName, parseDate, - translateFrom, useConfig, useLayoutType, usePatient, @@ -46,8 +45,8 @@ import type { MedicationRoute, QuantityUnit, } from '../types'; +import { useRequireOutpatientQuantity } from '../api/api'; import styles from './drug-order-form.scss'; -import { moduleName } from '../dashboard.meta'; export interface DrugOrderFormProps { initialOrderBasketItem: DrugOrderBasketItem; @@ -56,91 +55,115 @@ export interface DrugOrderFormProps { promptBeforeClosing: (testFcn: () => boolean) => void; } -const comboSchema = { - value: z.string(), - valueCoded: z.string(), - default: z.boolean().optional(), -}; +const createMedicationOrderFormSchema = (requireOutpatientQuantity: boolean, t: TFunction) => { + const comboSchema = { + default: z.boolean().optional(), + value: z.string(), + valueCoded: z.string(), + }; -const schemaFields = { - // t( 'freeDosageErrorMessage', 'Add free dosage note') - freeTextDosage: z.string().refine((value) => value !== '', { - message: translateFrom(moduleName, 'freeDosageErrorMessage', 'Add free dosage note'), - }), - - // t( 'dosageRequiredErrorMessage', 'A dosage is required' ) - dosage: z.number({ - invalid_type_error: translateFrom(moduleName, 'dosageRequiredErrorMessage', 'A dosage is required'), - }), - - // t( 'selectUnitErrorMessage', 'Please select a unit' ) - unit: z.object( - { ...comboSchema }, - { - invalid_type_error: translateFrom(moduleName, 'selectUnitErrorMessage', 'Please select a unit'), - }, - ), + const baseSchemaFields = { + freeTextDosage: z.string().refine((value) => !!value, { + message: t('freeDosageErrorMessage', 'Add free dosage note'), + }), + dosage: z.number({ + invalid_type_error: t('dosageRequiredErrorMessage', 'A dosage is required'), + }), + unit: z.object( + { ...comboSchema }, + { + invalid_type_error: t('selectUnitErrorMessage', 'Please select a unit'), + }, + ), + route: z.object( + { ...comboSchema }, + { + invalid_type_error: t('selectRouteErrorMessage', 'Please select a route'), + }, + ), + patientInstructions: z.string().nullable(), + asNeeded: z.boolean(), + asNeededCondition: z.string().nullable(), + duration: z.number().nullable(), + durationUnit: z.object({ ...comboSchema }).nullable(), + indication: z.string().refine((value) => value !== '', { + message: t('indicationErrorMessage', 'Please add an indication'), + }), + startDate: z.date(), + frequency: z.object( + { ...comboSchema }, + { + invalid_type_error: t('selectFrequencyErrorMessage', 'Please select a frequency'), + }, + ), + }; - // t( 'selectRouteErrorMessage', 'Please select a route' ) - route: z.object( - { ...comboSchema }, - { - invalid_type_error: translateFrom(moduleName, 'selectRouteErrorMessage', 'Please select a route'), - }, - ), - - patientInstructions: z.string().nullable(), - asNeeded: z.boolean(), - asNeededCondition: z.string().nullable(), - duration: z.number().nullable(), - durationUnit: z.object({ ...comboSchema }).nullable(), - // t( 'pillDispensedErrorMessage', 'The quantity to dispense is required' ) - pillsDispensed: z.number({ - invalid_type_error: translateFrom(moduleName, 'pillDispensedErrorMessage', 'The quantity to dispense is required'), - }), - // t( 'selectQuantityUnitsErrorMessage', 'Dispensing requires a quantity unit' ) - quantityUnits: z.object( - { ...comboSchema }, - { - invalid_type_error: translateFrom( - moduleName, - 'selectQuantityUnitsErrorMessage', - 'Dispensing requires a quantity unit', + const outpatientDrugOrderFields = { + pillsDispensed: z + .number() + .nullable() + .refine( + (value) => { + if (requireOutpatientQuantity && (typeof value !== 'number' || value < 1)) { + return false; + } + return true; + }, + { + message: t('pillDispensedErrorMessage', 'The quantity to dispense is required'), + }, ), - }, - ), - numRefills: z.number().nullable(), - // t( 'indicationErrorMessage', 'Please add an indication' ) - indication: z.string().refine((value) => value !== '', { - message: translateFrom(moduleName, 'indicationErrorMessage', 'Please add an indication'), - }), - startDate: z.date(), - // t( 'selectFrequencyErrorMessage', 'Please select a frequency' ) - frequency: z.object( - { ...comboSchema }, - { - invalid_type_error: translateFrom(moduleName, 'selectFrequencyErrorMessage', 'Please select a frequency'), - }, - ), -}; + quantityUnits: z + .object(comboSchema) + .nullable() + .refine( + (value) => { + if (requireOutpatientQuantity && !value) { + return false; + } + return true; + }, + { + message: t('selectQuantityUnitsErrorMessage', 'Dispensing requires a quantity unit'), + }, + ), + numRefills: z + .number() + .nullable() + .refine( + (value) => { + if (requireOutpatientQuantity && (typeof value !== 'number' || value < 0)) { + return false; + } + return true; + }, + { + message: t('numRefillsErrorMessage', 'The number of refills is required'), + }, + ), + }; -const medicationOrderFormSchema = z.discriminatedUnion('isFreeTextDosage', [ - z.object({ - ...schemaFields, + const nonFreeTextDosageSchema = z.object({ + ...baseSchemaFields, + ...outpatientDrugOrderFields, isFreeTextDosage: z.literal(false), freeTextDosage: z.string().optional(), - }), - z.object({ - ...schemaFields, + }); + + const freeTextDosageSchema = z.object({ + ...baseSchemaFields, + ...outpatientDrugOrderFields, isFreeTextDosage: z.literal(true), dosage: z.number().nullable(), - unit: z.object({ ...comboSchema }).nullable(), - route: z.object({ ...comboSchema }).nullable(), - frequency: z.object({ ...comboSchema }).nullable(), - }), -]); + unit: z.object(comboSchema).nullable(), + route: z.object(comboSchema).nullable(), + frequency: z.object(comboSchema).nullable(), + }); + + return z.discriminatedUnion('isFreeTextDosage', [nonFreeTextDosageSchema, freeTextDosageSchema]); +}; -type MedicationOrderFormData = z.infer; +type MedicationOrderFormData = z.infer>; function MedicationInfoHeader({ orderBasketItem, @@ -190,9 +213,10 @@ function InputWrapper({ children }) { export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, promptBeforeClosing }: DrugOrderFormProps) { const { t } = useTranslation(); + const config = useConfig(); const isTablet = useLayoutType() === 'tablet'; const { orderConfigObject, error: errorFetchingOrderConfig } = useOrderConfig(); - const config = useConfig() as ConfigObject; + const { requireOutpatientQuantity } = useRequireOutpatientQuantity(); const defaultStartDate = useMemo(() => { if (typeof initialOrderBasketItem?.startDate === 'string') parseDate(initialOrderBasketItem?.startDate); @@ -200,13 +224,18 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt return initialOrderBasketItem?.startDate as Date; }, [initialOrderBasketItem?.startDate]); + const medicationOrderFormSchema = useMemo( + () => createMedicationOrderFormSchema(requireOutpatientQuantity, t), + [requireOutpatientQuantity, t], + ); + const { - handleSubmit, control, - watch, - setValue, - formState: { isDirty, errors }, + formState: { isDirty }, getValues, + handleSubmit, + setValue, + watch, } = useForm({ mode: 'all', resolver: zodResolver(medicationOrderFormSchema), @@ -319,7 +348,7 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt const [showStickyMedicationHeader, setShowMedicationHeader] = useState(false); const { patient, isLoading: isLoadingPatientDetails } = usePatient(); const patientName = patient ? getPatientName(patient) : ''; - const { maxDispenseDurationInDays } = useConfig(); + const { maxDispenseDurationInDays } = useConfig(); const observer = useRef(null); const medicationInfoHeaderRef = useCallback( @@ -636,6 +665,7 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt label={t('quantityToDispense', 'Quantity to dispense')} min={0} hideSteppers + allowEmpty /> @@ -666,6 +696,7 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt min={0} label={t('prescriptionRefills', 'Prescription refills')} max={99} + allowEmpty /> ) : ( { const { field: { onBlur, onChange, value, ref }, - fieldState, + fieldState: { error }, } = useController({ name: name, control }); + const fieldErrorStyles = classNames({ + [styles.fieldError]: error?.message, + }); + const handleChange = useCallback( (newValue: MedicationOrderFormData[keyof MedicationOrderFormData]) => { const prevValue = getValues?.(name); @@ -811,11 +846,14 @@ const ControlledFieldInput = ({ if (type === 'number') return ( handleChange(parseFloat(value))} - className={fieldState?.error?.message && styles.fieldError} + className={fieldErrorStyles} onBlur={onBlur} + onChange={(e, { value }) => { + const number = parseFloat(value); + handleChange(isNaN(number) ? null : number); + }} ref={ref} + value={!!value ? value : 0} {...restProps} /> ); @@ -823,11 +861,11 @@ const ControlledFieldInput = ({ if (type === 'textArea') return (