Skip to content

Commit

Permalink
(feat) Add an empty state view to MarkPatientDeceased component (#2046)
Browse files Browse the repository at this point in the history
This PR extends the MarkPatientDeceased component as follows:

- Adds an empty state view to the component
- Adds test coverage for the component
- Amends the property passed to the `invalid` prop of the non-coded cause of death `TextInput` field.
  • Loading branch information
denniskigen authored Oct 1, 2024
1 parent 4b394c8 commit dc45ca5
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,30 @@
border-bottom: none;
}
}

.tileContainer {
background-color: $ui-02;
padding: layout.$spacing-09 0;
}

.tile {
margin: auto;
width: fit-content;
}

.tileContent {
display: flex;
flex-direction: column;
align-items: center;
}

.content {
@include type.type-style('heading-compact-02');
color: $text-02;
margin-bottom: layout.$spacing-03;
}

.helper {
@include type.type-style('body-compact-01');
color: $text-02;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { getDefaultsFromConfigSchema, showSnackbar, useConfig } from '@openmrs/esm-framework';
import { esmPatientChartSchema, type ChartConfig } from '../config-schema';
import { mockPatient } from 'tools';
import { markPatientDeceased, useCausesOfDeath } from '../data.resource';
import MarkPatientDeceasedForm from './mark-patient-deceased-form.workspace';

const originalLocation = window.location;
delete window.location;
window.location = { ...originalLocation, reload: jest.fn() };

const mockMarkPatientDeceased = jest.mocked(markPatientDeceased);
const mockUseCausesOfDeath = jest.mocked(useCausesOfDeath);
const mockUseConfig = jest.mocked(useConfig<ChartConfig>);
const mockShowSnackbar = jest.mocked(showSnackbar);
const mockCloseWorkspace = jest.fn();

jest.mock('../data.resource.ts', () => ({
markPatientDeceased: jest.fn().mockResolvedValue({}),
useCausesOfDeath: jest.fn(),
}));

describe('MarkPatientDeceasedForm', () => {
const freeTextFieldConceptUuid = '1234e218-6c8a-4ca3-8edb-9f6d9c8c8c7f';

const defaultProps = {
patientUuid: mockPatient.id,
closeWorkspace: mockCloseWorkspace,
closeWorkspaceWithSavedChanges: jest.fn(),
promptBeforeClosing: jest.fn(),
setTitle: jest.fn(),
};

const codedCausesOfDeath = [
{
display: 'Traumatic injury',
uuid: '8b64f45e-1d5f-4894-b77c-4e1d840e2c99',
name: 'Traumatic injury',
},
{
display: 'Neoplasm/cancer',
uuid: 'c4e8d03c-f09b-48d1-8d93-7d84d463f865',
name: 'Neoplasm/cancer',
},
{
display: 'Infectious disease',
uuid: 'b7c1c30f-5b9e-4a3d-b943-7f4b3f740e6c',
name: 'Infectious disease',
},
{
display: 'Other',
uuid: freeTextFieldConceptUuid,
name: 'Other',
},
];

beforeEach(() => {
mockUseCausesOfDeath.mockReturnValue({
causesOfDeath: codedCausesOfDeath,
isLoading: false,
isValidating: false,
});

mockUseConfig.mockReturnValue({
...getDefaultsFromConfigSchema(esmPatientChartSchema),
freeTextFieldConceptUuid,
});
});

afterAll(() => {
window.location = originalLocation;
});

it('renders the cause of death form', () => {
render(<MarkPatientDeceasedForm {...defaultProps} />);

expect(screen.getByRole('img', { name: /warning/i })).toBeInTheDocument();
expect(
screen.getByText(/marking the patient as deceased will end any active visits for this patient/i),
).toBeInTheDocument();
expect(screen.getByText(/cause of death/i)).toBeInTheDocument();
expect(screen.getByRole('searchbox')).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: /date/i }));
codedCausesOfDeath.forEach((codedCauseOfDeath) => {
expect(screen.getByRole('radio', { name: codedCauseOfDeath.display })).toBeInTheDocument();
});
expect(screen.getByRole('button', { name: /discard/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /save and close/i })).toBeInTheDocument();
});

it('searches through the list when the user types in the search input', async () => {
const user = userEvent.setup();

render(<MarkPatientDeceasedForm {...defaultProps} />);

const searchInput = screen.getByRole('searchbox');

await user.type(searchInput, 'totally random text');
expect(screen.getByText(/no matching coded causes of death/i));

await user.clear(searchInput);
await user.type(searchInput, 'traumatic injury');

expect(screen.getByRole('radio', { name: 'Traumatic injury' })).toBeInTheDocument();
expect(screen.getAllByRole('radio')).toHaveLength(1);
});

it('selecting "Other" as the cause of death requires the user to enter a non-coded cause of death', async () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
const user = userEvent.setup();

render(<MarkPatientDeceasedForm {...defaultProps} />);

const submitButton = screen.getByRole('button', { name: /save and close/i });

await user.click(screen.getByRole('radio', { name: 'Other' }));
expect(screen.getByRole('textbox', { name: /non-coded cause of death/i })).toBeInTheDocument();

await user.click(submitButton);

expect(screen.getByText(/please enter the non-coded cause of death/i)).toBeInTheDocument();

await user.type(screen.getByRole('textbox', { name: /non\-coded cause of death/i }), 'Septicemia');
await user.click(submitButton);

expect(markPatientDeceased).toHaveBeenCalledWith(
expect.any(Date),
'8673ee4f-e2ab-4077-ba55-4980f408773e', // causeOfDeathUuid
freeTextFieldConceptUuid, // otherCauseOfDeathConceptUuid
'Septicemia', // otherCauseOfDeath
);
consoleError.mockRestore();
});

it('submits the form with a coded cause of death', async () => {
const user = userEvent.setup();

render(<MarkPatientDeceasedForm {...defaultProps} />);

const submitButton = screen.getByRole('button', { name: /save and close/i });
const traumaticInjuryRadio = screen.getByRole('radio', { name: 'Traumatic injury' });

await user.click(traumaticInjuryRadio);
await user.click(submitButton);

expect(markPatientDeceased).toHaveBeenCalledWith(
expect.any(Date),
'8673ee4f-e2ab-4077-ba55-4980f408773e',
'8b64f45e-1d5f-4894-b77c-4e1d840e2c99', // causeOfDeathUuid for Traumatic injury,
'',
);
});

it('renders an error message when saving the cause of death fails', async () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
const user = userEvent.setup();
const mockError = new Error('API Error');

mockMarkPatientDeceased.mockRejectedValueOnce(mockError);

render(<MarkPatientDeceasedForm {...defaultProps} />);

const submitButton = screen.getByRole('button', { name: /save and close/i });
const traumaticInjuryRadio = screen.getByRole('radio', { name: 'Traumatic injury' });

await user.click(traumaticInjuryRadio);
await user.click(submitButton);

expect(mockShowSnackbar).toHaveBeenCalledWith({
isLowContrast: false,
kind: 'error',
subtitle: mockError.message,
title: 'Error marking patient deceased',
});
consoleError.mockRestore();
});

it('clicking the discard button closes the workspace', async () => {
const user = userEvent.setup();

render(<MarkPatientDeceasedForm {...defaultProps} />);

const discardButton = screen.getByRole('button', { name: /discard/i });
await user.click(discardButton);

expect(mockCloseWorkspace).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Search,
StructuredListSkeleton,
TextInput,
Tile,
} from '@carbon/react';
import { Controller, useForm, type SubmitHandler } from 'react-hook-form';
import { z } from 'zod';
Expand Down Expand Up @@ -50,6 +51,10 @@ const MarkPatientDeceasedForm: React.FC<DefaultPatientWorkspaceProps> = ({ close
: causesOfDeath;
}, [searchTerm, causesOfDeath]);

const handleSearchTermChange = (event) => {
setSearchTerm(event.target.value);
};

const schema = z
.object({
causeOfDeath: z.string().refine((causeOfDeath) => !!causeOfDeath, {
Expand Down Expand Up @@ -172,35 +177,55 @@ const MarkPatientDeceasedForm: React.FC<DefaultPatientWorkspaceProps> = ({ close
{causesOfDeath?.length ? (
<ResponsiveWrapper>
<Search
onChange={(event) => setSearchTerm(event.target.value)}
placeholder={t('searchForCauseOfDeath', 'Search for a cause of death')}
labelText=""
onChange={handleSearchTermChange}
placeholder={t('searchForCauseOfDeath', 'Search for a cause of death')}
/>
</ResponsiveWrapper>
) : null}

{causesOfDeath?.length ? (
{causesOfDeath?.length && filteredCausesOfDeath.length > 0 ? (
<Controller
name="causeOfDeath"
control={control}
render={({ field: { onChange } }) => (
<RadioButtonGroup className={styles.radioButtonGroup} orientation="vertical" onChange={onChange}>
{(filteredCausesOfDeath ? filteredCausesOfDeath : causesOfDeath).map(
({ uuid, display, name }) => (
<RadioButton
key={uuid}
className={styles.radioButton}
id={name}
labelText={display}
value={uuid}
/>
),
)}
<RadioButtonGroup
className={styles.radioButtonGroup}
name={
causeOfDeathValue === freeTextFieldConceptUuid
? 'freeTextFieldCauseOfDeath'
: 'codedCauseOfDeath'
}
orientation="vertical"
onChange={onChange}
>
{filteredCausesOfDeath.map(({ uuid, display, name }) => (
<RadioButton
className={styles.radioButton}
id={name}
key={uuid}
labelText={display}
value={uuid}
/>
))}
</RadioButtonGroup>
)}
/>
) : null}

{searchTerm && filteredCausesOfDeath.length === 0 && (
<div className={styles.tileContainer}>
<Tile className={styles.tile}>
<div className={styles.tileContent}>
<p className={styles.content}>
{t('noMatchingCodedCausesOfDeath', 'No matching coded causes of death')}
</p>
<p className={styles.helper}>{t('checkFilters', 'Check the filters above')}</p>
</div>
</Tile>
</div>
)}

{!isLoadingCausesOfDeath && !causesOfDeath?.length ? (
<EmptyState
displayText={t('causeOfDeath_lower', 'cause of death concepts configured in the system')}
Expand Down
1 change: 1 addition & 0 deletions packages/esm-patient-chart-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"noDiagnosesFound": "No diagnoses found",
"noEncountersFound": "No encounters found",
"noEncountersToDisplay": "No encounters to display",
"noMatchingCodedCausesOfDeath": "No matching coded causes of death",
"nonCodedCauseOfDeath": "Non-coded cause of death",
"nonCodedCauseOfDeathRequired": "Please enter the non-coded cause of death",
"noObservationsFound": "No observations found",
Expand Down

0 comments on commit dc45ca5

Please sign in to comment.