@bathiran212/esm-patient-notes-app 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/1076.js +1 -0
- package/dist/1076.js.map +1 -0
- package/dist/1339.js +1 -0
- package/dist/1339.js.map +1 -0
- package/dist/1480.js +1 -0
- package/dist/1480.js.map +1 -0
- package/dist/1646.js +1 -0
- package/dist/1646.js.map +1 -0
- package/dist/1789.js +1 -0
- package/dist/1789.js.map +1 -0
- package/dist/1869.js +1 -0
- package/dist/1869.js.map +1 -0
- package/dist/1871.js +1 -0
- package/dist/1871.js.map +1 -0
- package/dist/1877.js +1 -0
- package/dist/1877.js.map +1 -0
- package/dist/2153.js +1 -0
- package/dist/2153.js.map +1 -0
- package/dist/2317.js +1 -0
- package/dist/2317.js.map +1 -0
- package/dist/2416.js +1 -0
- package/dist/2416.js.map +1 -0
- package/dist/2544.js +27 -0
- package/dist/2544.js.map +1 -0
- package/dist/282.js +1 -0
- package/dist/282.js.map +1 -0
- package/dist/2824.js +1 -0
- package/dist/2824.js.map +1 -0
- package/dist/2842.js +1 -0
- package/dist/2842.js.map +1 -0
- package/dist/2881.js +1 -0
- package/dist/2881.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/3720.js +1 -0
- package/dist/3720.js.map +1 -0
- package/dist/3963.js +1 -0
- package/dist/3963.js.map +1 -0
- package/dist/3989.js +1 -0
- package/dist/3989.js.map +1 -0
- package/dist/4106.js +1 -0
- package/dist/4106.js.map +1 -0
- package/dist/4111.js +1 -0
- package/dist/4111.js.map +1 -0
- package/dist/434.js +1 -0
- package/dist/434.js.map +1 -0
- package/dist/4348.js +1 -0
- package/dist/4348.js.map +1 -0
- package/dist/4383.js +1 -0
- package/dist/4383.js.map +1 -0
- package/dist/4658.js +1 -0
- package/dist/4658.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/4928.js +1 -0
- package/dist/4928.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5132.js +1 -0
- package/dist/5132.js.map +1 -0
- package/dist/5145.js +1 -0
- package/dist/5145.js.map +1 -0
- package/dist/5503.js +1 -0
- package/dist/5503.js.map +1 -0
- package/dist/556.js +1 -0
- package/dist/556.js.map +1 -0
- package/dist/5644.js +1 -0
- package/dist/5644.js.map +1 -0
- package/dist/5697.js +1 -0
- package/dist/5697.js.map +1 -0
- package/dist/5861.js +1 -0
- package/dist/5861.js.map +1 -0
- package/dist/5940.js +1 -0
- package/dist/5940.js.map +1 -0
- package/dist/6047.js +1 -0
- package/dist/6047.js.map +1 -0
- package/dist/6371.js +1 -0
- package/dist/6371.js.map +1 -0
- package/dist/6377.js +1 -0
- package/dist/6377.js.map +1 -0
- package/dist/6444.js +1 -0
- package/dist/6444.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6724.js +1 -0
- package/dist/6724.js.map +1 -0
- package/dist/6904.js +1 -0
- package/dist/6904.js.map +1 -0
- package/dist/7045.js +1 -0
- package/dist/7045.js.map +1 -0
- package/dist/7103.js +1 -0
- package/dist/7103.js.map +1 -0
- package/dist/7175.js +1 -0
- package/dist/7175.js.map +1 -0
- package/dist/7182.js +1 -0
- package/dist/7182.js.map +1 -0
- package/dist/7205.js +11 -0
- package/dist/7205.js.map +1 -0
- package/dist/7646.js +17 -0
- package/dist/7646.js.map +1 -0
- package/dist/7742.js +1 -0
- package/dist/7742.js.map +1 -0
- package/dist/7912.js +1 -0
- package/dist/7912.js.map +1 -0
- package/dist/8358.js +1 -0
- package/dist/8358.js.map +1 -0
- package/dist/8359.js +1 -0
- package/dist/8359.js.map +1 -0
- package/dist/8369.js +1 -0
- package/dist/8369.js.map +1 -0
- package/dist/8695.js +1 -0
- package/dist/8695.js.map +1 -0
- package/dist/8722.js +1 -0
- package/dist/8722.js.map +1 -0
- package/dist/903.js +1 -0
- package/dist/903.js.map +1 -0
- package/dist/9061.js +1 -0
- package/dist/9061.js.map +1 -0
- package/dist/9072.js +1 -0
- package/dist/9072.js.map +1 -0
- package/dist/9105.js +1 -0
- package/dist/9105.js.map +1 -0
- package/dist/9712.js +1 -0
- package/dist/9712.js.map +1 -0
- package/dist/9771.js +1 -0
- package/dist/9771.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/main.js +16 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-patient-notes-app.js +6 -0
- package/dist/openmrs-esm-patient-notes-app.js.buildmanifest.json +1760 -0
- package/dist/openmrs-esm-patient-notes-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +58 -0
- package/rspack.config.js +1 -0
- package/src/config-schema.ts +28 -0
- package/src/dashboard.meta.ts +7 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +45 -0
- package/src/notes/notes-overview.extension.tsx +74 -0
- package/src/notes/notes-overview.scss +40 -0
- package/src/notes/notes-overview.test.tsx +101 -0
- package/src/notes/paginated-notes.component.tsx +182 -0
- package/src/notes/visit-note-config-schema.ts +38 -0
- package/src/notes/visit-notes-form.scss +219 -0
- package/src/notes/visit-notes-form.test.tsx +523 -0
- package/src/notes/visit-notes-form.workspace.tsx +853 -0
- package/src/notes/visit-notes.resource.ts +113 -0
- package/src/routes.json +48 -0
- package/src/sticky-notes/delete-sticky-note-button.component.tsx +39 -0
- package/src/sticky-notes/delete-sticky-note-button.scss +13 -0
- package/src/sticky-notes/delete-sticky-note.modal.test.tsx +72 -0
- package/src/sticky-notes/delete-sticky-note.modal.tsx +62 -0
- package/src/sticky-notes/edit-sticky-note-button.component.tsx +20 -0
- package/src/sticky-notes/sticky-note-header-button.component.tsx +100 -0
- package/src/sticky-notes/sticky-note-header-button.scss +38 -0
- package/src/sticky-notes/sticky-note-header-button.test.tsx +182 -0
- package/src/sticky-notes/sticky-note-panel.component.tsx +88 -0
- package/src/sticky-notes/sticky-note-panel.scss +54 -0
- package/src/sticky-notes/sticky-note-panel.test.tsx +66 -0
- package/src/sticky-notes/sticky-note.modal.scss +3 -0
- package/src/sticky-notes/sticky-note.modal.test.tsx +115 -0
- package/src/sticky-notes/sticky-note.modal.tsx +93 -0
- package/src/sticky-notes/sticky-note.resource.test.ts +24 -0
- package/src/sticky-notes/sticky-note.resource.ts +82 -0
- package/src/sticky-notes/utils.test.ts +36 -0
- package/src/sticky-notes/utils.ts +9 -0
- package/src/types/index.ts +203 -0
- package/src/visit-note-action-button.extension.tsx +28 -0
- package/src/visit-note-action-button.test.tsx +42 -0
- package/translations/am.json +55 -0
- package/translations/ar.json +55 -0
- package/translations/ar_SY.json +55 -0
- package/translations/bn.json +55 -0
- package/translations/cs.json +55 -0
- package/translations/de.json +55 -0
- package/translations/en.json +55 -0
- package/translations/en_US.json +55 -0
- package/translations/es.json +55 -0
- package/translations/es_MX.json +55 -0
- package/translations/fr.json +55 -0
- package/translations/he.json +55 -0
- package/translations/hi.json +55 -0
- package/translations/hi_IN.json +55 -0
- package/translations/id.json +55 -0
- package/translations/it.json +55 -0
- package/translations/ka.json +55 -0
- package/translations/km.json +55 -0
- package/translations/ku.json +55 -0
- package/translations/ky.json +55 -0
- package/translations/lg.json +55 -0
- package/translations/ne.json +55 -0
- package/translations/pl.json +55 -0
- package/translations/pt.json +55 -0
- package/translations/pt_BR.json +55 -0
- package/translations/qu.json +55 -0
- package/translations/ro_RO.json +55 -0
- package/translations/ru_RU.json +55 -0
- package/translations/si.json +55 -0
- package/translations/sq.json +55 -0
- package/translations/sw.json +55 -0
- package/translations/sw_KE.json +55 -0
- package/translations/tr.json +55 -0
- package/translations/tr_TR.json +55 -0
- package/translations/uk.json +55 -0
- package/translations/uz.json +55 -0
- package/translations/uz@Latn.json +55 -0
- package/translations/uz_UZ.json +55 -0
- package/translations/vi.json +55 -0
- package/translations/zh.json +55 -0
- package/translations/zh_CN.json +55 -0
- package/translations/zh_TW.json +55 -0
- package/tsconfig.json +4 -0
- package/vitest.config.ts +4 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* happy-dom's `AbortController` instances are not the host realm's
|
|
5
|
+
* `AbortController`, so `toHaveBeenCalledWith(new AbortController(), ...)`
|
|
6
|
+
* fails the cross-realm equality check used here.
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { vi, expect, test, beforeEach } from 'vitest';
|
|
10
|
+
import userEvent from '@testing-library/user-event';
|
|
11
|
+
import { screen, render } from '@testing-library/react';
|
|
12
|
+
import {
|
|
13
|
+
type Encounter,
|
|
14
|
+
getDefaultsFromConfigSchema,
|
|
15
|
+
showSnackbar,
|
|
16
|
+
useConfig,
|
|
17
|
+
useSession,
|
|
18
|
+
useFeatureFlag,
|
|
19
|
+
} from '@openmrs/esm-framework';
|
|
20
|
+
import { type PatientWorkspace2DefinitionProps } from '@openmrs/esm-patient-common-lib';
|
|
21
|
+
import { fetchDiagnosisConceptsByName, saveVisitNote, updateVisitNote } from './visit-notes.resource';
|
|
22
|
+
import {
|
|
23
|
+
ConfigMock,
|
|
24
|
+
diagnosisSearchResponse,
|
|
25
|
+
mockFetchLocationByUuidResponse,
|
|
26
|
+
mockFetchProviderByUuidResponse,
|
|
27
|
+
mockSessionDataResponse,
|
|
28
|
+
} from '__mocks__';
|
|
29
|
+
import { configSchema, type ConfigObject } from '../config-schema';
|
|
30
|
+
import { mockPatient, getByTextWithMarkup } from 'tools';
|
|
31
|
+
import VisitNotesForm, { type VisitNotesFormProps } from './visit-notes-form.workspace';
|
|
32
|
+
|
|
33
|
+
const defaultProps: PatientWorkspace2DefinitionProps<VisitNotesFormProps, {}> = {
|
|
34
|
+
closeWorkspace: vi.fn(),
|
|
35
|
+
workspaceProps: {
|
|
36
|
+
formContext: 'creating' as const,
|
|
37
|
+
},
|
|
38
|
+
groupProps: {
|
|
39
|
+
patient: mockPatient,
|
|
40
|
+
patientUuid: mockPatient.id,
|
|
41
|
+
visitContext: null,
|
|
42
|
+
mutateVisitContext: null,
|
|
43
|
+
},
|
|
44
|
+
launchChildWorkspace: vi.fn(),
|
|
45
|
+
windowProps: {},
|
|
46
|
+
workspaceName: '',
|
|
47
|
+
windowName: '',
|
|
48
|
+
isRootWorkspace: false,
|
|
49
|
+
showActionMenu: true,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function renderVisitNotesForm(workspaceProps: Partial<VisitNotesFormProps> = {}) {
|
|
53
|
+
const props = {
|
|
54
|
+
...defaultProps,
|
|
55
|
+
workspaceProps: { ...defaultProps.workspaceProps, ...workspaceProps },
|
|
56
|
+
};
|
|
57
|
+
render(<VisitNotesForm {...props} />);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const mockFetchDiagnosisConceptsByName = vi.mocked(fetchDiagnosisConceptsByName);
|
|
61
|
+
const mockSaveVisitNote = vi.mocked(saveVisitNote);
|
|
62
|
+
const mockShowSnackbar = vi.mocked(showSnackbar);
|
|
63
|
+
const mockUpdateVisitNote = vi.mocked(updateVisitNote);
|
|
64
|
+
const mockUseConfig = vi.mocked(useConfig<ConfigObject>);
|
|
65
|
+
const mockUseSession = vi.mocked(useSession);
|
|
66
|
+
const mockedUseFeatureFlag = vi.mocked(useFeatureFlag);
|
|
67
|
+
|
|
68
|
+
vi.mock('lodash-es/debounce', () => vi.fn((fn) => fn));
|
|
69
|
+
|
|
70
|
+
vi.mock('./visit-notes.resource', () => ({
|
|
71
|
+
fetchDiagnosisConceptsByName: vi.fn(),
|
|
72
|
+
updateVisitNote: vi.fn(),
|
|
73
|
+
useLocationUuid: vi.fn().mockImplementation(() => ({
|
|
74
|
+
data: mockFetchLocationByUuidResponse.data.uuid,
|
|
75
|
+
})),
|
|
76
|
+
useProviderUuid: vi.fn().mockImplementation(() => ({
|
|
77
|
+
data: mockFetchProviderByUuidResponse.data.uuid,
|
|
78
|
+
})),
|
|
79
|
+
saveVisitNote: vi.fn(),
|
|
80
|
+
useVisitNotes: vi.fn().mockImplementation(() => ({
|
|
81
|
+
mutateVisitNotes: vi.fn(),
|
|
82
|
+
})),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
mockUseSession.mockReturnValue(mockSessionDataResponse.data);
|
|
86
|
+
mockUseConfig.mockReturnValue({
|
|
87
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
88
|
+
...ConfigMock,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
mockedUseFeatureFlag.mockReturnValue(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('does not render the date picker when RDE is disabled', () => {
|
|
96
|
+
renderVisitNotesForm();
|
|
97
|
+
|
|
98
|
+
expect(screen.queryByLabelText(/visit date/i)).not.toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('renders the date picker when RDE is enabled', () => {
|
|
102
|
+
mockedUseFeatureFlag.mockReturnValue(true);
|
|
103
|
+
|
|
104
|
+
renderVisitNotesForm();
|
|
105
|
+
|
|
106
|
+
expect(screen.getByLabelText(/visit date/i)).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('renders the visit notes form with all the relevant fields and values', () => {
|
|
110
|
+
mockFetchDiagnosisConceptsByName.mockResolvedValue([]);
|
|
111
|
+
|
|
112
|
+
renderVisitNotesForm();
|
|
113
|
+
|
|
114
|
+
expect(screen.getByRole('textbox', { name: /write your notes/i })).toBeInTheDocument();
|
|
115
|
+
expect(screen.getByRole('searchbox', { name: /enter primary diagnoses/i })).toBeInTheDocument();
|
|
116
|
+
expect(screen.getByRole('searchbox', { name: /enter secondary diagnoses/i })).toBeInTheDocument();
|
|
117
|
+
expect(screen.getByRole('button', { name: /add image/i })).toBeInTheDocument();
|
|
118
|
+
expect(screen.getByRole('button', { name: /discard/i })).toBeInTheDocument();
|
|
119
|
+
expect(screen.getByRole('button', { name: /save and close/i })).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('typing in the diagnosis search input triggers a search', async () => {
|
|
123
|
+
const user = userEvent.setup();
|
|
124
|
+
|
|
125
|
+
mockFetchDiagnosisConceptsByName.mockResolvedValue(diagnosisSearchResponse.results);
|
|
126
|
+
|
|
127
|
+
renderVisitNotesForm();
|
|
128
|
+
|
|
129
|
+
const searchBox = screen.getByPlaceholderText('Choose a primary diagnosis');
|
|
130
|
+
await user.type(searchBox, 'Diabetes Mellitus');
|
|
131
|
+
|
|
132
|
+
// Wait for the search results to appear
|
|
133
|
+
const targetSearchResult = await screen.findByRole('menuitem', { name: 'Diabetes Mellitus' });
|
|
134
|
+
expect(targetSearchResult).toBeInTheDocument();
|
|
135
|
+
expect(screen.getByRole('menuitem', { name: 'Diabetes Mellitus, Type II' })).toBeInTheDocument();
|
|
136
|
+
|
|
137
|
+
// clicking on a search result displays the selected diagnosis as a tag
|
|
138
|
+
await user.click(targetSearchResult);
|
|
139
|
+
expect(screen.getByTitle('Diabetes Mellitus')).toBeInTheDocument();
|
|
140
|
+
const diabetesMellitusTag = screen.getByTitle(/^Diabetes Mellitus$/i);
|
|
141
|
+
expect(diabetesMellitusTag).toBeInTheDocument();
|
|
142
|
+
|
|
143
|
+
const closeTagButton = screen.getByRole('button', { name: /clear filter/i });
|
|
144
|
+
// Clicking the close button on the tag removes the selected diagnosis
|
|
145
|
+
await user.click(closeTagButton);
|
|
146
|
+
// no selected diagnoses left
|
|
147
|
+
expect(screen.getByText(/No diagnosis selected — Enter a diagnosis below/i)).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('renders an error message when no matching diagnoses are found', async () => {
|
|
151
|
+
const user = userEvent.setup();
|
|
152
|
+
mockFetchDiagnosisConceptsByName.mockResolvedValue([]);
|
|
153
|
+
|
|
154
|
+
renderVisitNotesForm();
|
|
155
|
+
|
|
156
|
+
const searchBox = screen.getByPlaceholderText('Choose a primary diagnosis');
|
|
157
|
+
await user.type(searchBox, 'COVID-21');
|
|
158
|
+
|
|
159
|
+
await screen.findByText(/No diagnoses found/i);
|
|
160
|
+
expect(getByTextWithMarkup('No diagnoses found matching "COVID-21"')).toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('closes the form and the workspace when the cancel button is clicked', async () => {
|
|
164
|
+
const user = userEvent.setup();
|
|
165
|
+
|
|
166
|
+
renderVisitNotesForm();
|
|
167
|
+
|
|
168
|
+
const cancelButton = screen.getByRole('button', { name: /Discard/i });
|
|
169
|
+
await user.click(cancelButton);
|
|
170
|
+
|
|
171
|
+
expect(defaultProps.closeWorkspace).toHaveBeenCalledTimes(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('renders a success snackbar upon successfully recording a visit note', async () => {
|
|
175
|
+
const user = userEvent.setup();
|
|
176
|
+
const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
177
|
+
|
|
178
|
+
const successPayload = {
|
|
179
|
+
encounterProviders: expect.arrayContaining([
|
|
180
|
+
{
|
|
181
|
+
encounterRole: ConfigMock.visitNoteConfig.clinicianEncounterRole,
|
|
182
|
+
provider: mockSessionDataResponse.data.currentProvider.uuid,
|
|
183
|
+
},
|
|
184
|
+
]),
|
|
185
|
+
encounterType: ConfigMock.visitNoteConfig.encounterTypeUuid,
|
|
186
|
+
form: ConfigMock.visitNoteConfig.formConceptUuid,
|
|
187
|
+
location: mockSessionDataResponse.data.sessionLocation.uuid,
|
|
188
|
+
obs: expect.arrayContaining([
|
|
189
|
+
{
|
|
190
|
+
concept: { display: '', uuid: '162169AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
|
|
191
|
+
value: 'Sample clinical note',
|
|
192
|
+
},
|
|
193
|
+
]),
|
|
194
|
+
patient: mockPatient.id,
|
|
195
|
+
encounterDatetime: undefined,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
mockSaveVisitNote.mockResolvedValueOnce({ status: 201, body: 'Condition created' } as unknown as Awaited<
|
|
199
|
+
ReturnType<typeof saveVisitNote>
|
|
200
|
+
>);
|
|
201
|
+
mockFetchDiagnosisConceptsByName.mockResolvedValue(diagnosisSearchResponse.results);
|
|
202
|
+
|
|
203
|
+
renderVisitNotesForm();
|
|
204
|
+
|
|
205
|
+
const clinicalNote = screen.getByRole('textbox', { name: /Write your notes/i });
|
|
206
|
+
await user.type(clinicalNote, 'x');
|
|
207
|
+
const submitButton = screen.getByRole('button', { name: /Save and close/i });
|
|
208
|
+
await user.click(submitButton);
|
|
209
|
+
|
|
210
|
+
expect(screen.getByText(/choose at least one primary diagnosis/i)).toBeInTheDocument();
|
|
211
|
+
|
|
212
|
+
await user.clear(clinicalNote);
|
|
213
|
+
const searchBox = screen.getByPlaceholderText('Choose a primary diagnosis');
|
|
214
|
+
await user.type(searchBox, 'Diabetes Mellitus');
|
|
215
|
+
const targetSearchResult = await screen.findByText('Diabetes Mellitus');
|
|
216
|
+
expect(targetSearchResult).toBeInTheDocument();
|
|
217
|
+
|
|
218
|
+
await user.click(targetSearchResult);
|
|
219
|
+
|
|
220
|
+
await user.clear(clinicalNote);
|
|
221
|
+
await user.type(clinicalNote, 'Sample clinical note');
|
|
222
|
+
expect(clinicalNote).toHaveValue('Sample clinical note');
|
|
223
|
+
|
|
224
|
+
await user.click(submitButton);
|
|
225
|
+
|
|
226
|
+
expect(mockSaveVisitNote).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(mockSaveVisitNote).toHaveBeenCalledWith(new AbortController(), expect.objectContaining(successPayload));
|
|
228
|
+
mockConsoleError.mockRestore();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('renders an error snackbar if there was a problem recording a condition', async () => {
|
|
232
|
+
const user = userEvent.setup();
|
|
233
|
+
|
|
234
|
+
const error = {
|
|
235
|
+
message: 'Internal Server Error',
|
|
236
|
+
response: {
|
|
237
|
+
status: 500,
|
|
238
|
+
statusText: 'Internal Server Error',
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
mockSaveVisitNote.mockRejectedValueOnce(error);
|
|
243
|
+
mockFetchDiagnosisConceptsByName.mockResolvedValue(diagnosisSearchResponse.results);
|
|
244
|
+
|
|
245
|
+
renderVisitNotesForm();
|
|
246
|
+
|
|
247
|
+
const submitButton = screen.getByRole('button', { name: /Save and close/i });
|
|
248
|
+
|
|
249
|
+
const searchBox = screen.getByPlaceholderText('Choose a primary diagnosis');
|
|
250
|
+
await user.type(searchBox, 'Diabetes Mellitus');
|
|
251
|
+
const targetSearchResult = await screen.findByText('Diabetes Mellitus');
|
|
252
|
+
expect(targetSearchResult).toBeInTheDocument();
|
|
253
|
+
|
|
254
|
+
await user.click(targetSearchResult);
|
|
255
|
+
|
|
256
|
+
const clinicalNote = screen.getByRole('textbox', { name: /Write your notes/i });
|
|
257
|
+
await user.clear(clinicalNote);
|
|
258
|
+
await user.type(clinicalNote, 'Sample clinical note');
|
|
259
|
+
expect(clinicalNote).toHaveValue('Sample clinical note');
|
|
260
|
+
|
|
261
|
+
await user.click(submitButton);
|
|
262
|
+
|
|
263
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith({
|
|
264
|
+
isLowContrast: false,
|
|
265
|
+
kind: 'error',
|
|
266
|
+
subtitle: 'Internal Server Error',
|
|
267
|
+
title: 'Error saving visit note',
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('initializes form with existing encounter data when in edit mode', () => {
|
|
272
|
+
mockedUseFeatureFlag.mockReturnValue(true);
|
|
273
|
+
|
|
274
|
+
const mockEncounter = {
|
|
275
|
+
id: '123',
|
|
276
|
+
uuid: '123',
|
|
277
|
+
datetime: '20/03/2024',
|
|
278
|
+
rawDatetime: '2024-03-20T10:00:00.000Z',
|
|
279
|
+
obs: [
|
|
280
|
+
{
|
|
281
|
+
concept: { uuid: '162169AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
|
|
282
|
+
value: 'Existing clinical note',
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
diagnoses: [
|
|
286
|
+
{
|
|
287
|
+
uuid: '456',
|
|
288
|
+
diagnosis: {
|
|
289
|
+
coded: { uuid: '789', display: 'Diabetes Mellitus' },
|
|
290
|
+
},
|
|
291
|
+
certainty: 'PROVISIONAL',
|
|
292
|
+
rank: 1,
|
|
293
|
+
display: 'Diabetes Mellitus',
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
renderVisitNotesForm({
|
|
299
|
+
formContext: 'editing',
|
|
300
|
+
encounter: mockEncounter as any as Encounter, // TODO: fix
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Verify date is pre-filled
|
|
304
|
+
expect(screen.getByLabelText(/visit date/i)).toHaveValue('20/03/2024');
|
|
305
|
+
|
|
306
|
+
// Verify clinical note is pre-filled
|
|
307
|
+
expect(screen.getByRole('textbox', { name: /write your notes/i })).toHaveValue('Existing clinical note');
|
|
308
|
+
|
|
309
|
+
// Verify diagnosis is pre-filled
|
|
310
|
+
expect(screen.getByTitle('Diabetes Mellitus')).toBeInTheDocument();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('updates existing visit note when in edit mode', async () => {
|
|
314
|
+
const user = userEvent.setup();
|
|
315
|
+
const mockEncounter = {
|
|
316
|
+
id: '123',
|
|
317
|
+
uuid: '123',
|
|
318
|
+
datetime: '20/03/2024',
|
|
319
|
+
rawDatetime: '2024-03-20T10:00:00.000Z',
|
|
320
|
+
obs: [
|
|
321
|
+
{
|
|
322
|
+
concept: { uuid: '162169AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
|
|
323
|
+
value: 'Existing clinical note',
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
diagnoses: [
|
|
327
|
+
{
|
|
328
|
+
uuid: '456',
|
|
329
|
+
diagnosis: {
|
|
330
|
+
coded: { uuid: '789', display: 'Diabetes Mellitus' },
|
|
331
|
+
},
|
|
332
|
+
certainty: 'PROVISIONAL',
|
|
333
|
+
rank: 1,
|
|
334
|
+
display: 'Diabetes Mellitus',
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const updatePayload = {
|
|
340
|
+
encounterProviders: [
|
|
341
|
+
{
|
|
342
|
+
encounterRole: ConfigMock.visitNoteConfig.clinicianEncounterRole,
|
|
343
|
+
provider: mockSessionDataResponse.data.currentProvider.uuid,
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
encounterType: ConfigMock.visitNoteConfig.encounterTypeUuid,
|
|
347
|
+
form: ConfigMock.visitNoteConfig.formConceptUuid,
|
|
348
|
+
location: mockSessionDataResponse.data.sessionLocation.uuid,
|
|
349
|
+
obs: [
|
|
350
|
+
{
|
|
351
|
+
concept: { display: '', uuid: '162169AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
|
|
352
|
+
value: 'Updated clinical note',
|
|
353
|
+
uuid: undefined,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
patient: mockPatient.id,
|
|
357
|
+
encounterDatetime: undefined,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
mockFetchDiagnosisConceptsByName.mockResolvedValue(diagnosisSearchResponse.results);
|
|
361
|
+
mockUpdateVisitNote.mockResolvedValueOnce({ status: 200, body: 'Visit note updated' } as unknown as Awaited<
|
|
362
|
+
ReturnType<typeof updateVisitNote>
|
|
363
|
+
>);
|
|
364
|
+
|
|
365
|
+
renderVisitNotesForm({
|
|
366
|
+
formContext: 'editing',
|
|
367
|
+
encounter: mockEncounter as any as Encounter, // TODO: fix
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Update clinical note
|
|
371
|
+
const clinicalNote = screen.getByRole('textbox', { name: /Write your notes/i });
|
|
372
|
+
await user.clear(clinicalNote);
|
|
373
|
+
await user.type(clinicalNote, 'Updated clinical note');
|
|
374
|
+
expect(clinicalNote).toHaveValue('Updated clinical note');
|
|
375
|
+
|
|
376
|
+
// Submit form
|
|
377
|
+
const submitButton = screen.getByRole('button', { name: /Save and close/i });
|
|
378
|
+
await user.click(submitButton);
|
|
379
|
+
|
|
380
|
+
expect(mockUpdateVisitNote).toHaveBeenCalledWith(
|
|
381
|
+
expect.any(AbortController),
|
|
382
|
+
mockEncounter.id,
|
|
383
|
+
expect.objectContaining(updatePayload),
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('handles existing diagnoses correctly when in edit mode', async () => {
|
|
388
|
+
const user = userEvent.setup();
|
|
389
|
+
const mockEncounter = {
|
|
390
|
+
id: '123',
|
|
391
|
+
uuid: '123',
|
|
392
|
+
datetime: '20/03/2024',
|
|
393
|
+
rawDatetime: '2024-03-20T10:00:00.000Z',
|
|
394
|
+
diagnoses: [
|
|
395
|
+
{
|
|
396
|
+
uuid: '456',
|
|
397
|
+
diagnosis: {
|
|
398
|
+
coded: { uuid: '789', display: 'Diabetes Mellitus' },
|
|
399
|
+
},
|
|
400
|
+
certainty: 'PROVISIONAL',
|
|
401
|
+
rank: 1,
|
|
402
|
+
display: 'Diabetes Mellitus',
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
mockFetchDiagnosisConceptsByName.mockResolvedValue(diagnosisSearchResponse.results);
|
|
408
|
+
|
|
409
|
+
renderVisitNotesForm({
|
|
410
|
+
formContext: 'editing',
|
|
411
|
+
encounter: mockEncounter,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Verify existing diagnosis is displayed
|
|
415
|
+
expect(screen.getByTitle('Diabetes Mellitus')).toBeInTheDocument();
|
|
416
|
+
|
|
417
|
+
// Remove existing diagnosis
|
|
418
|
+
const closeTagButton = screen.getByRole('button', { name: /clear filter/i });
|
|
419
|
+
await user.click(closeTagButton);
|
|
420
|
+
|
|
421
|
+
// Verify no diagnoses are selected
|
|
422
|
+
expect(screen.getByText(/No diagnosis selected — Enter a diagnosis below/i)).toBeInTheDocument();
|
|
423
|
+
|
|
424
|
+
// Add new diagnosis
|
|
425
|
+
const searchBox = screen.getByPlaceholderText('Choose a primary diagnosis');
|
|
426
|
+
await user.type(searchBox, 'Diabetes Mellitus');
|
|
427
|
+
const targetSearchResult = await screen.findByText('Diabetes Mellitus');
|
|
428
|
+
await user.click(targetSearchResult);
|
|
429
|
+
|
|
430
|
+
// Verify new diagnosis is displayed
|
|
431
|
+
expect(screen.getByTitle('Diabetes Mellitus')).toBeInTheDocument();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('allows saving visit note without primary diagnosis when isPrimaryDiagnosisRequired is false', async () => {
|
|
435
|
+
const user = userEvent.setup();
|
|
436
|
+
|
|
437
|
+
mockUseConfig.mockReturnValue({
|
|
438
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
439
|
+
...ConfigMock,
|
|
440
|
+
isPrimaryDiagnosisRequired: false,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const successPayload = {
|
|
444
|
+
encounterProviders: expect.arrayContaining([
|
|
445
|
+
{
|
|
446
|
+
encounterRole: ConfigMock.visitNoteConfig.clinicianEncounterRole,
|
|
447
|
+
provider: mockSessionDataResponse.data.currentProvider.uuid,
|
|
448
|
+
},
|
|
449
|
+
]),
|
|
450
|
+
encounterType: ConfigMock.visitNoteConfig.encounterTypeUuid,
|
|
451
|
+
form: ConfigMock.visitNoteConfig.formConceptUuid,
|
|
452
|
+
location: mockSessionDataResponse.data.sessionLocation.uuid,
|
|
453
|
+
obs: expect.arrayContaining([
|
|
454
|
+
{
|
|
455
|
+
concept: { display: '', uuid: '162169AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
|
|
456
|
+
value: 'Clinical note without diagnosis',
|
|
457
|
+
},
|
|
458
|
+
]),
|
|
459
|
+
patient: mockPatient.id,
|
|
460
|
+
encounterDatetime: undefined,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
mockSaveVisitNote.mockResolvedValueOnce({ status: 201, body: 'Visit note created' } as unknown as Awaited<
|
|
464
|
+
ReturnType<typeof saveVisitNote>
|
|
465
|
+
>);
|
|
466
|
+
mockFetchDiagnosisConceptsByName.mockResolvedValue(diagnosisSearchResponse.results);
|
|
467
|
+
|
|
468
|
+
renderVisitNotesForm();
|
|
469
|
+
|
|
470
|
+
const clinicalNote = screen.getByRole('textbox', { name: /Write your notes/i });
|
|
471
|
+
await user.clear(clinicalNote);
|
|
472
|
+
await user.type(clinicalNote, 'Clinical note without diagnosis');
|
|
473
|
+
expect(clinicalNote).toHaveValue('Clinical note without diagnosis');
|
|
474
|
+
|
|
475
|
+
const submitButton = screen.getByRole('button', { name: /Save and close/i });
|
|
476
|
+
await user.click(submitButton);
|
|
477
|
+
|
|
478
|
+
// Should not show validation error for missing primary diagnosis
|
|
479
|
+
expect(screen.queryByText(/choose at least one primary diagnosis/i)).not.toBeInTheDocument();
|
|
480
|
+
|
|
481
|
+
// Should successfully save the visit note
|
|
482
|
+
expect(mockSaveVisitNote).toHaveBeenCalledTimes(1);
|
|
483
|
+
expect(mockSaveVisitNote).toHaveBeenCalledWith(new AbortController(), expect.objectContaining(successPayload));
|
|
484
|
+
|
|
485
|
+
// Reset mock for other tests
|
|
486
|
+
mockUseConfig.mockReturnValue({
|
|
487
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
488
|
+
...ConfigMock,
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test('requires primary diagnosis when isPrimaryDiagnosisRequired is true', async () => {
|
|
493
|
+
const user = userEvent.setup();
|
|
494
|
+
|
|
495
|
+
mockUseConfig.mockReturnValue({
|
|
496
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
497
|
+
...ConfigMock,
|
|
498
|
+
isPrimaryDiagnosisRequired: true,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
mockFetchDiagnosisConceptsByName.mockResolvedValue(diagnosisSearchResponse.results);
|
|
502
|
+
|
|
503
|
+
renderVisitNotesForm();
|
|
504
|
+
|
|
505
|
+
const clinicalNote = screen.getByRole('textbox', { name: /Write your notes/i });
|
|
506
|
+
await user.clear(clinicalNote);
|
|
507
|
+
await user.type(clinicalNote, 'Clinical note without diagnosis');
|
|
508
|
+
|
|
509
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
510
|
+
await user.click(submitButton);
|
|
511
|
+
|
|
512
|
+
// Should show validation error for missing primary diagnosis
|
|
513
|
+
expect(screen.getByText(/choose at least one primary diagnosis/i)).toBeInTheDocument();
|
|
514
|
+
|
|
515
|
+
// Should not attempt to save
|
|
516
|
+
expect(mockSaveVisitNote).not.toHaveBeenCalled();
|
|
517
|
+
|
|
518
|
+
// Reset mock for other tests
|
|
519
|
+
mockUseConfig.mockReturnValue({
|
|
520
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
521
|
+
...ConfigMock,
|
|
522
|
+
});
|
|
523
|
+
});
|