@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.
Files changed (216) hide show
  1. package/README.md +4 -0
  2. package/dist/1076.js +1 -0
  3. package/dist/1076.js.map +1 -0
  4. package/dist/1339.js +1 -0
  5. package/dist/1339.js.map +1 -0
  6. package/dist/1480.js +1 -0
  7. package/dist/1480.js.map +1 -0
  8. package/dist/1646.js +1 -0
  9. package/dist/1646.js.map +1 -0
  10. package/dist/1789.js +1 -0
  11. package/dist/1789.js.map +1 -0
  12. package/dist/1869.js +1 -0
  13. package/dist/1869.js.map +1 -0
  14. package/dist/1871.js +1 -0
  15. package/dist/1871.js.map +1 -0
  16. package/dist/1877.js +1 -0
  17. package/dist/1877.js.map +1 -0
  18. package/dist/2153.js +1 -0
  19. package/dist/2153.js.map +1 -0
  20. package/dist/2317.js +1 -0
  21. package/dist/2317.js.map +1 -0
  22. package/dist/2416.js +1 -0
  23. package/dist/2416.js.map +1 -0
  24. package/dist/2544.js +27 -0
  25. package/dist/2544.js.map +1 -0
  26. package/dist/282.js +1 -0
  27. package/dist/282.js.map +1 -0
  28. package/dist/2824.js +1 -0
  29. package/dist/2824.js.map +1 -0
  30. package/dist/2842.js +1 -0
  31. package/dist/2842.js.map +1 -0
  32. package/dist/2881.js +1 -0
  33. package/dist/2881.js.map +1 -0
  34. package/dist/3378.js +1 -0
  35. package/dist/3378.js.map +1 -0
  36. package/dist/3720.js +1 -0
  37. package/dist/3720.js.map +1 -0
  38. package/dist/3963.js +1 -0
  39. package/dist/3963.js.map +1 -0
  40. package/dist/3989.js +1 -0
  41. package/dist/3989.js.map +1 -0
  42. package/dist/4106.js +1 -0
  43. package/dist/4106.js.map +1 -0
  44. package/dist/4111.js +1 -0
  45. package/dist/4111.js.map +1 -0
  46. package/dist/434.js +1 -0
  47. package/dist/434.js.map +1 -0
  48. package/dist/4348.js +1 -0
  49. package/dist/4348.js.map +1 -0
  50. package/dist/4383.js +1 -0
  51. package/dist/4383.js.map +1 -0
  52. package/dist/4658.js +1 -0
  53. package/dist/4658.js.map +1 -0
  54. package/dist/466.js +1 -0
  55. package/dist/466.js.map +1 -0
  56. package/dist/4928.js +1 -0
  57. package/dist/4928.js.map +1 -0
  58. package/dist/5117.js +1 -0
  59. package/dist/5117.js.map +1 -0
  60. package/dist/5132.js +1 -0
  61. package/dist/5132.js.map +1 -0
  62. package/dist/5145.js +1 -0
  63. package/dist/5145.js.map +1 -0
  64. package/dist/5503.js +1 -0
  65. package/dist/5503.js.map +1 -0
  66. package/dist/556.js +1 -0
  67. package/dist/556.js.map +1 -0
  68. package/dist/5644.js +1 -0
  69. package/dist/5644.js.map +1 -0
  70. package/dist/5697.js +1 -0
  71. package/dist/5697.js.map +1 -0
  72. package/dist/5861.js +1 -0
  73. package/dist/5861.js.map +1 -0
  74. package/dist/5940.js +1 -0
  75. package/dist/5940.js.map +1 -0
  76. package/dist/6047.js +1 -0
  77. package/dist/6047.js.map +1 -0
  78. package/dist/6371.js +1 -0
  79. package/dist/6371.js.map +1 -0
  80. package/dist/6377.js +1 -0
  81. package/dist/6377.js.map +1 -0
  82. package/dist/6444.js +1 -0
  83. package/dist/6444.js.map +1 -0
  84. package/dist/6508.js +1 -0
  85. package/dist/6508.js.map +1 -0
  86. package/dist/6724.js +1 -0
  87. package/dist/6724.js.map +1 -0
  88. package/dist/6904.js +1 -0
  89. package/dist/6904.js.map +1 -0
  90. package/dist/7045.js +1 -0
  91. package/dist/7045.js.map +1 -0
  92. package/dist/7103.js +1 -0
  93. package/dist/7103.js.map +1 -0
  94. package/dist/7175.js +1 -0
  95. package/dist/7175.js.map +1 -0
  96. package/dist/7182.js +1 -0
  97. package/dist/7182.js.map +1 -0
  98. package/dist/7205.js +11 -0
  99. package/dist/7205.js.map +1 -0
  100. package/dist/7646.js +17 -0
  101. package/dist/7646.js.map +1 -0
  102. package/dist/7742.js +1 -0
  103. package/dist/7742.js.map +1 -0
  104. package/dist/7912.js +1 -0
  105. package/dist/7912.js.map +1 -0
  106. package/dist/8358.js +1 -0
  107. package/dist/8358.js.map +1 -0
  108. package/dist/8359.js +1 -0
  109. package/dist/8359.js.map +1 -0
  110. package/dist/8369.js +1 -0
  111. package/dist/8369.js.map +1 -0
  112. package/dist/8695.js +1 -0
  113. package/dist/8695.js.map +1 -0
  114. package/dist/8722.js +1 -0
  115. package/dist/8722.js.map +1 -0
  116. package/dist/903.js +1 -0
  117. package/dist/903.js.map +1 -0
  118. package/dist/9061.js +1 -0
  119. package/dist/9061.js.map +1 -0
  120. package/dist/9072.js +1 -0
  121. package/dist/9072.js.map +1 -0
  122. package/dist/9105.js +1 -0
  123. package/dist/9105.js.map +1 -0
  124. package/dist/9712.js +1 -0
  125. package/dist/9712.js.map +1 -0
  126. package/dist/9771.js +1 -0
  127. package/dist/9771.js.map +1 -0
  128. package/dist/9806.js +1 -0
  129. package/dist/9806.js.map +1 -0
  130. package/dist/main.js +16 -0
  131. package/dist/main.js.map +1 -0
  132. package/dist/openmrs-esm-patient-notes-app.js +6 -0
  133. package/dist/openmrs-esm-patient-notes-app.js.buildmanifest.json +1760 -0
  134. package/dist/openmrs-esm-patient-notes-app.js.map +1 -0
  135. package/dist/routes.json +1 -0
  136. package/package.json +58 -0
  137. package/rspack.config.js +1 -0
  138. package/src/config-schema.ts +28 -0
  139. package/src/dashboard.meta.ts +7 -0
  140. package/src/declarations.d.ts +4 -0
  141. package/src/index.ts +45 -0
  142. package/src/notes/notes-overview.extension.tsx +74 -0
  143. package/src/notes/notes-overview.scss +40 -0
  144. package/src/notes/notes-overview.test.tsx +101 -0
  145. package/src/notes/paginated-notes.component.tsx +182 -0
  146. package/src/notes/visit-note-config-schema.ts +38 -0
  147. package/src/notes/visit-notes-form.scss +219 -0
  148. package/src/notes/visit-notes-form.test.tsx +523 -0
  149. package/src/notes/visit-notes-form.workspace.tsx +853 -0
  150. package/src/notes/visit-notes.resource.ts +113 -0
  151. package/src/routes.json +48 -0
  152. package/src/sticky-notes/delete-sticky-note-button.component.tsx +39 -0
  153. package/src/sticky-notes/delete-sticky-note-button.scss +13 -0
  154. package/src/sticky-notes/delete-sticky-note.modal.test.tsx +72 -0
  155. package/src/sticky-notes/delete-sticky-note.modal.tsx +62 -0
  156. package/src/sticky-notes/edit-sticky-note-button.component.tsx +20 -0
  157. package/src/sticky-notes/sticky-note-header-button.component.tsx +100 -0
  158. package/src/sticky-notes/sticky-note-header-button.scss +38 -0
  159. package/src/sticky-notes/sticky-note-header-button.test.tsx +182 -0
  160. package/src/sticky-notes/sticky-note-panel.component.tsx +88 -0
  161. package/src/sticky-notes/sticky-note-panel.scss +54 -0
  162. package/src/sticky-notes/sticky-note-panel.test.tsx +66 -0
  163. package/src/sticky-notes/sticky-note.modal.scss +3 -0
  164. package/src/sticky-notes/sticky-note.modal.test.tsx +115 -0
  165. package/src/sticky-notes/sticky-note.modal.tsx +93 -0
  166. package/src/sticky-notes/sticky-note.resource.test.ts +24 -0
  167. package/src/sticky-notes/sticky-note.resource.ts +82 -0
  168. package/src/sticky-notes/utils.test.ts +36 -0
  169. package/src/sticky-notes/utils.ts +9 -0
  170. package/src/types/index.ts +203 -0
  171. package/src/visit-note-action-button.extension.tsx +28 -0
  172. package/src/visit-note-action-button.test.tsx +42 -0
  173. package/translations/am.json +55 -0
  174. package/translations/ar.json +55 -0
  175. package/translations/ar_SY.json +55 -0
  176. package/translations/bn.json +55 -0
  177. package/translations/cs.json +55 -0
  178. package/translations/de.json +55 -0
  179. package/translations/en.json +55 -0
  180. package/translations/en_US.json +55 -0
  181. package/translations/es.json +55 -0
  182. package/translations/es_MX.json +55 -0
  183. package/translations/fr.json +55 -0
  184. package/translations/he.json +55 -0
  185. package/translations/hi.json +55 -0
  186. package/translations/hi_IN.json +55 -0
  187. package/translations/id.json +55 -0
  188. package/translations/it.json +55 -0
  189. package/translations/ka.json +55 -0
  190. package/translations/km.json +55 -0
  191. package/translations/ku.json +55 -0
  192. package/translations/ky.json +55 -0
  193. package/translations/lg.json +55 -0
  194. package/translations/ne.json +55 -0
  195. package/translations/pl.json +55 -0
  196. package/translations/pt.json +55 -0
  197. package/translations/pt_BR.json +55 -0
  198. package/translations/qu.json +55 -0
  199. package/translations/ro_RO.json +55 -0
  200. package/translations/ru_RU.json +55 -0
  201. package/translations/si.json +55 -0
  202. package/translations/sq.json +55 -0
  203. package/translations/sw.json +55 -0
  204. package/translations/sw_KE.json +55 -0
  205. package/translations/tr.json +55 -0
  206. package/translations/tr_TR.json +55 -0
  207. package/translations/uk.json +55 -0
  208. package/translations/uz.json +55 -0
  209. package/translations/uz@Latn.json +55 -0
  210. package/translations/uz_UZ.json +55 -0
  211. package/translations/vi.json +55 -0
  212. package/translations/zh.json +55 -0
  213. package/translations/zh_CN.json +55 -0
  214. package/translations/zh_TW.json +55 -0
  215. package/tsconfig.json +4 -0
  216. 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
+ });