@openmrs/esm-generic-patient-widgets-app 11.3.1-pre.9398 → 11.3.1-pre.9403

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 (38) hide show
  1. package/.turbo/turbo-build.log +16 -16
  2. package/dist/1936.js +1 -0
  3. package/dist/1936.js.map +1 -0
  4. package/dist/2606.js +2 -0
  5. package/dist/2606.js.map +1 -0
  6. package/dist/4300.js +1 -1
  7. package/dist/5670.js +1 -1
  8. package/dist/7545.js +2 -0
  9. package/dist/7545.js.map +1 -0
  10. package/dist/8803.js +1 -1
  11. package/dist/main.js +1 -1
  12. package/dist/main.js.map +1 -1
  13. package/dist/openmrs-esm-generic-patient-widgets-app.js +1 -1
  14. package/dist/openmrs-esm-generic-patient-widgets-app.js.buildmanifest.json +91 -91
  15. package/dist/openmrs-esm-generic-patient-widgets-app.js.map +1 -1
  16. package/dist/routes.json +1 -1
  17. package/package.json +2 -2
  18. package/src/config-schema-obs-horizontal.ts +12 -0
  19. package/src/obs-graph/obs-graph.component.tsx +12 -12
  20. package/src/obs-switchable/obs-switchable.component.tsx +5 -9
  21. package/src/obs-switchable/obs-switchable.test.tsx +22 -12
  22. package/src/obs-table/obs-table.component.tsx +2 -1
  23. package/src/obs-table-horizontal/obs-table-horizontal.component.tsx +466 -56
  24. package/src/obs-table-horizontal/obs-table-horizontal.resource.ts +67 -0
  25. package/src/obs-table-horizontal/obs-table-horizontal.scss +47 -0
  26. package/src/obs-table-horizontal/obs-table-horizontal.test.tsx +912 -0
  27. package/src/resources/useConcepts.ts +14 -4
  28. package/src/resources/useEncounterTypes.ts +34 -0
  29. package/src/resources/useObs.ts +29 -47
  30. package/translations/en.json +6 -1
  31. package/dist/251.js +0 -2
  32. package/dist/251.js.map +0 -1
  33. package/dist/8743.js +0 -2
  34. package/dist/8743.js.map +0 -1
  35. package/dist/9351.js +0 -1
  36. package/dist/9351.js.map +0 -1
  37. /package/dist/{251.js.LICENSE.txt → 2606.js.LICENSE.txt} +0 -0
  38. /package/dist/{8743.js.LICENSE.txt → 7545.js.LICENSE.txt} +0 -0
@@ -0,0 +1,912 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor, within } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import {
5
+ getDefaultsFromConfigSchema,
6
+ useConfig,
7
+ showSnackbar,
8
+ useLayoutType,
9
+ isDesktop,
10
+ useSession,
11
+ userHasAccess,
12
+ } from '@openmrs/esm-framework';
13
+ import ObsTableHorizontal from './obs-table-horizontal.component';
14
+ import { useObs, type ObsResult } from '../resources/useObs';
15
+ import { configSchemaHorizontal } from '../config-schema-obs-horizontal';
16
+ import { updateObservation, createObservationInEncounter, createEncounter } from './obs-table-horizontal.resource';
17
+ import { useEncounterTypes } from '../resources/useEncounterTypes';
18
+
19
+ jest.mock('../resources/useObs', () => ({
20
+ useObs: jest.fn(),
21
+ }));
22
+
23
+ jest.mock('../resources/useEncounterTypes', () => ({
24
+ useEncounterTypes: jest.fn(),
25
+ }));
26
+
27
+ jest.mock('./obs-table-horizontal.resource', () => ({
28
+ updateObservation: jest.fn(),
29
+ createObservationInEncounter: jest.fn(),
30
+ createEncounter: jest.fn(),
31
+ }));
32
+
33
+ jest.mock('@openmrs/esm-framework', () => {
34
+ const originalModule = jest.requireActual('@openmrs/esm-framework');
35
+ return {
36
+ ...originalModule,
37
+ showSnackbar: jest.fn(),
38
+ useLayoutType: jest.fn(),
39
+ isDesktop: jest.fn(),
40
+ useSession: jest.fn(),
41
+ userHasAccess: jest.fn(() => true),
42
+ formatDate: jest.fn((date, options) => {
43
+ if (options?.time) return 'Jan 1, 2021, 12:00 AM';
44
+ return 'Jan 1, 2021';
45
+ }),
46
+ formatTime: jest.fn(() => '12:00 AM'),
47
+ };
48
+ });
49
+
50
+ const mockUseObs = jest.mocked(useObs);
51
+ const mockUseConfig = jest.mocked(useConfig);
52
+ const mockShowSnackbar = jest.mocked(showSnackbar);
53
+ const mockUpdateObservation = jest.mocked(updateObservation);
54
+ const mockCreateObservationInEncounter = jest.mocked(createObservationInEncounter);
55
+ const mockCreateEncounter = jest.mocked(createEncounter);
56
+ const mockUseLayoutType = jest.mocked(useLayoutType);
57
+ const mockIsDesktop = jest.mocked(isDesktop);
58
+ const mockUseSession = jest.mocked(useSession);
59
+ const mockUseEncounterTypes = jest.mocked(useEncounterTypes);
60
+
61
+ const mockObsData = [
62
+ {
63
+ id: 'obs-1',
64
+ code: { text: 'Height', coding: [{ code: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }] },
65
+ conceptUuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
66
+ dataType: 'Numeric',
67
+ effectiveDateTime: '2021-02-01T00:00:00Z',
68
+ valueQuantity: { value: 182 },
69
+ encounter: { reference: 'Encounter/234', name: 'Outpatient' },
70
+ },
71
+ {
72
+ id: 'obs-2',
73
+ code: { text: 'Weight', coding: [{ code: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }] },
74
+ conceptUuid: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
75
+ dataType: 'Numeric',
76
+ effectiveDateTime: '2021-02-01T00:00:00Z',
77
+ valueQuantity: { value: 72 },
78
+ encounter: { reference: 'Encounter/234', name: 'Outpatient' },
79
+ },
80
+ {
81
+ id: 'obs-3',
82
+ code: { text: 'Height', coding: [{ code: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }] },
83
+ conceptUuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
84
+ dataType: 'Numeric',
85
+ effectiveDateTime: '2021-01-01T00:00:00Z',
86
+ valueQuantity: { value: 180 },
87
+ encounter: { reference: 'Encounter/123', name: 'Inpatient' },
88
+ },
89
+ {
90
+ id: 'obs-4',
91
+ code: { text: 'Weight', coding: [{ code: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }] },
92
+ conceptUuid: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
93
+ dataType: 'Numeric',
94
+ effectiveDateTime: '2021-01-01T00:00:00Z',
95
+ valueQuantity: { value: 70 },
96
+ encounter: { reference: 'Encounter/123', name: 'Inpatient' },
97
+ },
98
+ {
99
+ id: 'obs-5',
100
+ code: { text: 'Chief Complaint', coding: [{ code: '164162AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }] },
101
+ conceptUuid: '164162AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
102
+ dataType: 'Text',
103
+ effectiveDateTime: '2021-01-01T00:00:00Z',
104
+ valueString: 'Headache',
105
+ encounter: { reference: 'Encounter/123', name: 'Inpatient' },
106
+ },
107
+ {
108
+ id: 'obs-6',
109
+ code: { text: 'Diagnosis', coding: [{ code: '1284AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }] },
110
+ conceptUuid: '1284AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
111
+ dataType: 'Coded',
112
+ effectiveDateTime: '2021-01-01T00:00:00Z',
113
+ valueCodeableConcept: {
114
+ coding: [{ code: 'answer-uuid-1', display: 'Malaria' }],
115
+ },
116
+ encounter: { reference: 'Encounter/123', name: 'Inpatient' },
117
+ },
118
+ ] as Array<ObsResult>;
119
+
120
+ const mockConceptData = [
121
+ { uuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Height', dataType: 'Numeric' },
122
+ { uuid: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Weight', dataType: 'Numeric' },
123
+ { uuid: '164162AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Chief Complaint', dataType: 'Text' },
124
+ {
125
+ uuid: '1284AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
126
+ display: 'Diagnosis',
127
+ dataType: 'Coded',
128
+ answers: [
129
+ { uuid: 'answer-uuid-1', display: 'Malaria' },
130
+ { uuid: 'answer-uuid-2', display: 'Typhoid' },
131
+ { uuid: 'answer-uuid-3', display: 'Pneumonia' },
132
+ ],
133
+ },
134
+ ];
135
+
136
+ const mockEncounters = [
137
+ { reference: 'Encounter/123', display: 'Inpatient', encounterTypeUuid: 'encounter-type-uuid-1' },
138
+ { reference: 'Encounter/234', display: 'Outpatient', encounterTypeUuid: 'encounter-type-uuid-2' },
139
+ ];
140
+
141
+ describe('ObsTableHorizontal', () => {
142
+ beforeEach(() => {
143
+ jest.clearAllMocks();
144
+ mockUseLayoutType.mockReturnValue('small-desktop');
145
+ mockIsDesktop.mockReturnValue(true);
146
+ mockUseEncounterTypes.mockReturnValue({
147
+ encounterTypes: [
148
+ { uuid: 'encounter-type-uuid-1', editPrivilege: { uuid: 'privilege-1', display: 'Edit Privilege 1' } },
149
+ { uuid: 'encounter-type-uuid-2', editPrivilege: { uuid: 'privilege-2', display: 'Edit Privilege 2' } },
150
+ ],
151
+ error: null,
152
+ isLoading: false,
153
+ isValidating: false,
154
+ mutate: jest.fn(),
155
+ });
156
+ });
157
+
158
+ it('should render observations in a horizontal table', () => {
159
+ mockUseObs.mockReturnValue({
160
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
161
+ error: null,
162
+ isLoading: false,
163
+ isValidating: false,
164
+ mutate: jest.fn(),
165
+ });
166
+ mockUseConfig.mockReturnValue({
167
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
168
+ title: 'Vitals',
169
+ editable: false,
170
+ data: [
171
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
172
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
173
+ ],
174
+ });
175
+
176
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
177
+
178
+ expect(screen.getByText('Height')).toBeInTheDocument();
179
+ expect(screen.getByText('Weight')).toBeInTheDocument();
180
+ expect(screen.getByText('182')).toBeInTheDocument();
181
+ expect(screen.getByText('72')).toBeInTheDocument();
182
+ });
183
+ });
184
+
185
+ describe('ObsTableHorizontal editable mode', () => {
186
+ beforeEach(() => {
187
+ jest.clearAllMocks();
188
+ mockUseLayoutType.mockReturnValue('small-desktop');
189
+ mockIsDesktop.mockReturnValue(true);
190
+ mockUseSession.mockReturnValue({
191
+ sessionLocation: { uuid: 'location-uuid-123' },
192
+ user: { uuid: 'user-uuid-123' },
193
+ } as any);
194
+ mockUseEncounterTypes.mockReturnValue({
195
+ encounterTypes: [
196
+ { uuid: 'encounter-type-uuid-1', editPrivilege: { uuid: 'privilege-1', display: 'Edit Privilege 1' } },
197
+ { uuid: 'encounter-type-uuid-2', editPrivilege: { uuid: 'privilege-2', display: 'Edit Privilege 2' } },
198
+ ],
199
+ error: null,
200
+ isLoading: false,
201
+ isValidating: false,
202
+ mutate: jest.fn(),
203
+ });
204
+ mockUpdateObservation.mockResolvedValue({ data: {} } as any);
205
+ mockCreateObservationInEncounter.mockResolvedValue({ data: {} } as any);
206
+ mockCreateEncounter.mockResolvedValue({ data: { uuid: 'new-encounter-uuid' } } as any);
207
+ });
208
+
209
+ it('should show edit button on hover when editable is true', async () => {
210
+ const user = userEvent.setup();
211
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
212
+
213
+ mockUseObs.mockReturnValue({
214
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
215
+ error: null,
216
+ isLoading: false,
217
+ isValidating: false,
218
+ mutate: mockMutate,
219
+ });
220
+ mockUseConfig.mockReturnValue({
221
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
222
+ title: 'Vitals',
223
+ editable: true,
224
+ data: [
225
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
226
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
227
+ ],
228
+ });
229
+
230
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
231
+
232
+ const heightCell = screen.getByRole('cell', { name: /182/ });
233
+ expect(heightCell).toBeInTheDocument();
234
+
235
+ // Hover over the cell to trigger the edit button visibility
236
+ await user.hover(heightCell);
237
+ // The edit button should be in the document (even if opacity is 0, it's still in DOM)
238
+ const editButtons = screen.queryAllByLabelText('Edit');
239
+ expect(editButtons.length).toBeGreaterThan(0);
240
+ });
241
+
242
+ it('should not show edit button when editable is false', () => {
243
+ mockUseObs.mockReturnValue({
244
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
245
+ error: null,
246
+ isLoading: false,
247
+ isValidating: false,
248
+ mutate: jest.fn(),
249
+ });
250
+ mockUseConfig.mockReturnValue({
251
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
252
+ title: 'Vitals',
253
+ editable: false,
254
+ data: [
255
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
256
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
257
+ ],
258
+ });
259
+
260
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
261
+
262
+ const editButtons = screen.queryAllByLabelText('Edit');
263
+ expect(editButtons).toHaveLength(0);
264
+ });
265
+
266
+ it("should show 'tap to edit' message when editable is true and on tablet", () => {
267
+ mockUseLayoutType.mockReturnValue('tablet');
268
+ mockIsDesktop.mockReturnValue(false);
269
+ mockUseObs.mockReturnValue({
270
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
271
+ error: null,
272
+ isLoading: false,
273
+ isValidating: false,
274
+ mutate: jest.fn(),
275
+ });
276
+ mockUseConfig.mockReturnValue({
277
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
278
+ title: 'Vitals',
279
+ editable: true,
280
+ });
281
+
282
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
283
+ expect(screen.getByText(/tap an observation to edit/i)).toBeInTheDocument();
284
+ });
285
+
286
+ it('should open input field when edit button is clicked', async () => {
287
+ const user = userEvent.setup();
288
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
289
+
290
+ mockUseObs.mockReturnValue({
291
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
292
+ error: null,
293
+ isLoading: false,
294
+ isValidating: false,
295
+ mutate: mockMutate,
296
+ });
297
+ mockUseConfig.mockReturnValue({
298
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
299
+ title: 'Vitals',
300
+ editable: true,
301
+ data: [
302
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
303
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
304
+ ],
305
+ });
306
+
307
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
308
+
309
+ const heightCell = screen.getByRole('cell', { name: /182/ });
310
+ await user.hover(heightCell);
311
+ const editButton = within(heightCell).getByRole('button', { name: 'Edit' });
312
+ await user.click(editButton);
313
+
314
+ await waitFor(() => {
315
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
316
+ });
317
+ const input = screen.getByRole('spinbutton');
318
+ expect(input).toHaveValue(182);
319
+ });
320
+
321
+ it('should update existing observation when value is changed and saved', async () => {
322
+ const user = userEvent.setup();
323
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
324
+
325
+ mockUseObs.mockReturnValue({
326
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
327
+ error: null,
328
+ isLoading: false,
329
+ isValidating: false,
330
+ mutate: mockMutate,
331
+ });
332
+ mockUseConfig.mockReturnValue({
333
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
334
+ title: 'Vitals',
335
+ editable: true,
336
+ data: [
337
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
338
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
339
+ ],
340
+ });
341
+
342
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
343
+
344
+ const heightCell = screen.getByRole('cell', { name: /182/ });
345
+ await user.hover(heightCell);
346
+ const editButton = within(heightCell).getByRole('button', { name: 'Edit' });
347
+ await user.click(editButton);
348
+
349
+ await waitFor(() => {
350
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
351
+ });
352
+
353
+ const input = screen.getByRole('spinbutton');
354
+ await user.clear(input);
355
+ await user.type(input, '185');
356
+
357
+ const saveButton = screen.getByRole('button', { name: 'Save' });
358
+ await user.click(saveButton);
359
+
360
+ await waitFor(() => {
361
+ expect(mockUpdateObservation).toHaveBeenCalledWith('obs-1', 185);
362
+ });
363
+ expect(mockMutate).toHaveBeenCalled();
364
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
365
+ expect.objectContaining({
366
+ kind: 'success',
367
+ title: 'Observation saved successfully',
368
+ }),
369
+ );
370
+ });
371
+
372
+ it('should create new observation when editing empty cell', async () => {
373
+ const user = userEvent.setup();
374
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
375
+
376
+ // Create obs data without weight observation
377
+ const obsDataWithoutWeight = mockObsData.filter(
378
+ (obs) => obs.conceptUuid !== '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
379
+ );
380
+
381
+ mockUseObs.mockReturnValue({
382
+ data: { observations: obsDataWithoutWeight, concepts: mockConceptData, encounters: mockEncounters },
383
+ error: null,
384
+ isLoading: false,
385
+ isValidating: false,
386
+ mutate: mockMutate,
387
+ });
388
+ mockUseConfig.mockReturnValue({
389
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
390
+ title: 'Vitals',
391
+ editable: true,
392
+ data: [
393
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
394
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
395
+ ],
396
+ });
397
+
398
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
399
+
400
+ // Find an empty cell in the Weight row (should show '--' or '-- Edit')
401
+ const weightRow = screen.getByRole('row', { name: /Weight/i });
402
+ const allCells = within(weightRow).getAllByRole('cell');
403
+ // Skip the first cell (label) and find a cell that contains '--' (empty value cell)
404
+ const weightEmptyCell = allCells.slice(1).find((cell) => {
405
+ const cellText = cell.textContent || '';
406
+ return cellText.includes('--');
407
+ });
408
+ expect(weightEmptyCell).toBeInTheDocument();
409
+
410
+ await user.hover(weightEmptyCell);
411
+ const editButton = within(weightEmptyCell).getByRole('button', { name: 'Edit' });
412
+ await user.click(editButton);
413
+
414
+ await waitFor(() => {
415
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
416
+ });
417
+
418
+ const input = screen.getByRole('spinbutton');
419
+ await user.type(input, '75');
420
+
421
+ const saveButton = screen.getByRole('button', { name: 'Save' });
422
+ await user.click(saveButton);
423
+
424
+ await waitFor(() => {
425
+ expect(mockCreateObservationInEncounter).toHaveBeenCalledWith(
426
+ '234',
427
+ 'patient-123',
428
+ '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
429
+ 75,
430
+ );
431
+ });
432
+ expect(mockMutate).toHaveBeenCalled();
433
+ });
434
+
435
+ it('should cancel editing when cancel button is clicked', async () => {
436
+ const user = userEvent.setup();
437
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
438
+
439
+ mockUseObs.mockReturnValue({
440
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
441
+ error: null,
442
+ isLoading: false,
443
+ isValidating: false,
444
+ mutate: mockMutate,
445
+ });
446
+ mockUseConfig.mockReturnValue({
447
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
448
+ title: 'Vitals',
449
+ editable: true,
450
+ data: [
451
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
452
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
453
+ ],
454
+ });
455
+
456
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
457
+
458
+ const heightCell = screen.getByRole('cell', { name: /182/ });
459
+ await user.hover(heightCell);
460
+ const editButton = within(heightCell).getByRole('button', { name: 'Edit' });
461
+ await user.click(editButton);
462
+
463
+ await waitFor(() => {
464
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
465
+ });
466
+
467
+ const input = screen.getByRole('spinbutton');
468
+ await user.clear(input);
469
+ await user.type(input, '185');
470
+
471
+ // Click cancel button
472
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
473
+ await user.click(cancelButton);
474
+
475
+ // Verify that the input is no longer visible and value was not saved
476
+ await waitFor(() => {
477
+ expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument();
478
+ });
479
+ expect(mockUpdateObservation).not.toHaveBeenCalled();
480
+ expect(mockMutate).not.toHaveBeenCalled();
481
+ // Verify the original value is still displayed
482
+ expect(screen.getByRole('cell', { name: /182/ })).toBeInTheDocument();
483
+ });
484
+
485
+ it('should cancel editing when Escape key is pressed', async () => {
486
+ const user = userEvent.setup();
487
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
488
+
489
+ mockUseObs.mockReturnValue({
490
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
491
+ error: null,
492
+ isLoading: false,
493
+ isValidating: false,
494
+ mutate: mockMutate,
495
+ });
496
+ mockUseConfig.mockReturnValue({
497
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
498
+ title: 'Vitals',
499
+ editable: true,
500
+ data: [
501
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
502
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
503
+ ],
504
+ });
505
+
506
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
507
+
508
+ const heightCell = screen.getByRole('cell', { name: /182/ });
509
+ await user.hover(heightCell);
510
+ const editButton = within(heightCell).getByRole('button', { name: 'Edit' });
511
+ await user.click(editButton);
512
+
513
+ await waitFor(() => {
514
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
515
+ });
516
+
517
+ const input = screen.getByRole('spinbutton');
518
+ await user.type(input, '{Escape}');
519
+
520
+ await waitFor(() => {
521
+ expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument();
522
+ });
523
+ expect(mockUpdateObservation).not.toHaveBeenCalled();
524
+ });
525
+
526
+ it('should save when Enter key is pressed', async () => {
527
+ const user = userEvent.setup();
528
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
529
+
530
+ mockUseObs.mockReturnValue({
531
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
532
+ error: null,
533
+ isLoading: false,
534
+ isValidating: false,
535
+ mutate: mockMutate,
536
+ });
537
+ mockUseConfig.mockReturnValue({
538
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
539
+ title: 'Vitals',
540
+ editable: true,
541
+ data: [
542
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
543
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
544
+ ],
545
+ });
546
+
547
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
548
+
549
+ const heightCell = screen.getByRole('cell', { name: /182/ });
550
+ await user.hover(heightCell);
551
+ const editButton = within(heightCell).getByRole('button', { name: 'Edit' });
552
+ await user.click(editButton);
553
+
554
+ await waitFor(() => {
555
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
556
+ });
557
+
558
+ const input = screen.getByRole('spinbutton');
559
+ await user.clear(input);
560
+ await user.type(input, '185');
561
+ await user.type(input, '{Enter}');
562
+
563
+ await waitFor(() => {
564
+ expect(mockUpdateObservation).toHaveBeenCalledWith('obs-1', 185);
565
+ });
566
+ expect(mockMutate).toHaveBeenCalled();
567
+ });
568
+
569
+ it('should handle text observations', async () => {
570
+ const user = userEvent.setup();
571
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
572
+
573
+ mockUseObs.mockReturnValue({
574
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
575
+ error: null,
576
+ isLoading: false,
577
+ isValidating: false,
578
+ mutate: mockMutate,
579
+ });
580
+ mockUseConfig.mockReturnValue({
581
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
582
+ title: 'Vitals',
583
+ editable: true,
584
+ data: [{ concept: '164162AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Chief Complaint' }],
585
+ });
586
+
587
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
588
+
589
+ const complaintCell = screen.getByRole('cell', { name: /Headache/ });
590
+ await user.hover(complaintCell);
591
+ const editButton = within(complaintCell).getByRole('button', { name: 'Edit' });
592
+ await user.click(editButton);
593
+
594
+ await waitFor(() => {
595
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
596
+ });
597
+ const textInput = screen.getByRole('textbox');
598
+ expect(textInput).toHaveValue('Headache');
599
+
600
+ await user.clear(textInput);
601
+ await user.type(textInput, 'Fever');
602
+
603
+ const saveButton = screen.getByRole('button', { name: 'Save' });
604
+ await user.click(saveButton);
605
+
606
+ await waitFor(() => {
607
+ expect(mockUpdateObservation).toHaveBeenCalledWith('obs-5', 'Fever');
608
+ });
609
+ expect(mockMutate).toHaveBeenCalled();
610
+ });
611
+
612
+ it('should handle coded observations', async () => {
613
+ const user = userEvent.setup();
614
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
615
+
616
+ mockUseObs.mockReturnValue({
617
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
618
+ error: null,
619
+ isLoading: false,
620
+ isValidating: false,
621
+ mutate: mockMutate,
622
+ });
623
+ mockUseConfig.mockReturnValue({
624
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
625
+ title: 'Diagnosis',
626
+ editable: true,
627
+ data: [{ concept: '1284AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Diagnosis' }],
628
+ });
629
+
630
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
631
+
632
+ const diagnosisCell = screen.getByRole('cell', { name: /Malaria/ });
633
+ expect(diagnosisCell).toBeInTheDocument();
634
+
635
+ await user.hover(diagnosisCell);
636
+ const editButton = within(diagnosisCell).getByRole('button', { name: 'Edit' });
637
+ await user.click(editButton);
638
+
639
+ await waitFor(() => {
640
+ expect(within(diagnosisCell).getByRole('combobox')).toBeInTheDocument();
641
+ });
642
+ const select = within(diagnosisCell).getByRole('combobox');
643
+ expect(select).toHaveValue('answer-uuid-1');
644
+
645
+ // Change to a different answer
646
+ await user.selectOptions(select, 'answer-uuid-2');
647
+
648
+ const saveButton = screen.getByRole('button', { name: 'Save' });
649
+ await user.click(saveButton);
650
+
651
+ await waitFor(() => {
652
+ expect(mockUpdateObservation).toHaveBeenCalledWith('obs-6', 'answer-uuid-2');
653
+ });
654
+ expect(mockMutate).toHaveBeenCalled();
655
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
656
+ expect.objectContaining({
657
+ kind: 'success',
658
+ title: 'Observation saved successfully',
659
+ }),
660
+ );
661
+ });
662
+
663
+ it('should not save when value is unchanged', async () => {
664
+ const user = userEvent.setup();
665
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
666
+
667
+ mockUseObs.mockReturnValue({
668
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
669
+ error: null,
670
+ isLoading: false,
671
+ isValidating: false,
672
+ mutate: mockMutate,
673
+ });
674
+ mockUseConfig.mockReturnValue({
675
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
676
+ title: 'Vitals',
677
+ editable: true,
678
+ data: [
679
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
680
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
681
+ ],
682
+ });
683
+
684
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
685
+
686
+ const heightCell = screen.getByRole('cell', { name: /182/ });
687
+ await user.hover(heightCell);
688
+ const editButton = within(heightCell).getByRole('button', { name: 'Edit' });
689
+ await user.click(editButton);
690
+
691
+ await waitFor(() => {
692
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
693
+ });
694
+
695
+ // Click save without changing value - should not save
696
+ const saveButton = screen.getByRole('button', { name: 'Save' });
697
+ await user.click(saveButton);
698
+
699
+ await waitFor(() => {
700
+ expect(mockUpdateObservation).not.toHaveBeenCalled();
701
+ });
702
+ expect(mockMutate).not.toHaveBeenCalled();
703
+ });
704
+
705
+ it('should not create observation when editing empty cell and nothing is entered', async () => {
706
+ const user = userEvent.setup();
707
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
708
+
709
+ // Create obs data where one encounter is missing a Weight observation
710
+ // This will create an empty cell in an existing encounter
711
+ const obsDataWithoutWeightInOneEncounter = mockObsData.filter(
712
+ (obs) =>
713
+ !(obs.conceptUuid === '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' && obs.encounter.reference === 'Encounter/234'),
714
+ );
715
+
716
+ mockUseObs.mockReturnValue({
717
+ data: { observations: obsDataWithoutWeightInOneEncounter, concepts: mockConceptData, encounters: mockEncounters },
718
+ error: null,
719
+ isLoading: false,
720
+ isValidating: false,
721
+ mutate: mockMutate,
722
+ });
723
+ mockUseConfig.mockReturnValue({
724
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
725
+ title: 'Vitals',
726
+ editable: true,
727
+ data: [
728
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
729
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
730
+ ],
731
+ });
732
+
733
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
734
+
735
+ // Find an empty cell in the Weight row (should show '--')
736
+ const weightRow = screen.getByRole('row', { name: /Weight/i });
737
+ const allCells = within(weightRow).getAllByRole('cell');
738
+ const emptyCell = allCells.slice(1).find((cell) => {
739
+ const cellText = cell.textContent || '';
740
+ return cellText.includes('--');
741
+ });
742
+ expect(emptyCell).toBeInTheDocument();
743
+
744
+ // Hover and click the edit button
745
+ await user.hover(emptyCell);
746
+ const editButton = within(emptyCell).getByRole('button', { name: 'Edit' });
747
+ await user.click(editButton);
748
+
749
+ // Wait for the input to appear
750
+ await waitFor(() => {
751
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
752
+ });
753
+
754
+ // Click cancel without entering any value
755
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
756
+ await user.click(cancelButton);
757
+
758
+ // Wait a bit to ensure no API calls are made
759
+ await waitFor(() => {
760
+ expect(mockCreateObservationInEncounter).not.toHaveBeenCalled();
761
+ });
762
+
763
+ expect(mockCreateEncounter).not.toHaveBeenCalled();
764
+ expect(mockMutate).not.toHaveBeenCalled();
765
+ });
766
+
767
+ it('should create and save encounter', async () => {
768
+ const user = userEvent.setup();
769
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
770
+ const newEncounterUuid = 'new-encounter-uuid-123';
771
+
772
+ mockUseObs.mockReturnValue({
773
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
774
+ error: null,
775
+ isLoading: false,
776
+ isValidating: false,
777
+ mutate: mockMutate,
778
+ });
779
+ mockUseConfig.mockReturnValue({
780
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
781
+ title: 'Vitals',
782
+ editable: true,
783
+ encounterTypeToCreateUuid: 'dd528487-82a5-4082-9c72-ed246bd49591',
784
+ data: [
785
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
786
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
787
+ ],
788
+ });
789
+
790
+ mockCreateEncounter.mockResolvedValue({
791
+ data: { uuid: newEncounterUuid },
792
+ } as any);
793
+
794
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
795
+
796
+ // Click the "+" button to add a new encounter
797
+ const addButton = screen.getByRole('button', { name: /add encounter/i });
798
+ await user.click(addButton);
799
+
800
+ // Wait for the new column to appear
801
+ await waitFor(() => {
802
+ const headers = screen.getAllByRole('columnheader');
803
+ expect(headers.length).toBeGreaterThan(2); // Should have label column + existing columns + new column
804
+ });
805
+
806
+ expect(mockCreateEncounter).not.toHaveBeenCalled();
807
+
808
+ // Find an empty cell in the Weight row in the new temporary encounter
809
+ const weightRow = screen.getByRole('row', { name: /Weight/i });
810
+ const allCells = within(weightRow).getAllByRole('cell');
811
+ // Find a cell that contains '--' (empty value cell) - should be the last one (new temporary encounter)
812
+ const emptyCell = allCells.slice(1).find((cell) => {
813
+ const cellText = cell.textContent || '';
814
+ return cellText.includes('--');
815
+ });
816
+ expect(emptyCell).toBeInTheDocument();
817
+
818
+ // Hover and click the edit button
819
+ await user.hover(emptyCell);
820
+ const editButton = within(emptyCell).getByRole('button', { name: 'Edit' });
821
+ await user.click(editButton);
822
+
823
+ // Wait for the input to appear
824
+ await waitFor(() => {
825
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
826
+ });
827
+
828
+ // Enter a value
829
+ const input = screen.getByRole('spinbutton');
830
+ await user.type(input, '75');
831
+
832
+ // Click save button
833
+ const saveButton = screen.getByRole('button', { name: 'Save' });
834
+ await user.click(saveButton);
835
+
836
+ // Verify that createEncounter was called with correct parameters
837
+ await waitFor(() => {
838
+ expect(mockCreateEncounter).toHaveBeenCalledWith(
839
+ 'patient-123',
840
+ 'dd528487-82a5-4082-9c72-ed246bd49591',
841
+ 'location-uuid-123',
842
+ [{ concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 75 }],
843
+ );
844
+ });
845
+
846
+ // Verify that mutate was called (via onEncounterCreated)
847
+ await waitFor(() => {
848
+ expect(mockMutate).toHaveBeenCalled();
849
+ });
850
+
851
+ // Verify success snackbar is shown
852
+ await waitFor(() => {
853
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
854
+ expect.objectContaining({
855
+ kind: 'success',
856
+ title: 'Observation saved successfully',
857
+ }),
858
+ );
859
+ });
860
+ });
861
+
862
+ it('should show error snackbar when update fails', async () => {
863
+ const user = userEvent.setup();
864
+ const mockMutate = jest.fn().mockResolvedValue(undefined);
865
+ const error = new Error('Update failed');
866
+ mockUpdateObservation.mockRejectedValue(error);
867
+
868
+ mockUseObs.mockReturnValue({
869
+ data: { observations: mockObsData, concepts: mockConceptData, encounters: mockEncounters },
870
+ error: null,
871
+ isLoading: false,
872
+ isValidating: false,
873
+ mutate: mockMutate,
874
+ });
875
+ mockUseConfig.mockReturnValue({
876
+ ...(getDefaultsFromConfigSchema(configSchemaHorizontal) as Object),
877
+ title: 'Vitals',
878
+ editable: true,
879
+ data: [
880
+ { concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Height' },
881
+ { concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', label: 'Weight' },
882
+ ],
883
+ });
884
+
885
+ render(<ObsTableHorizontal patientUuid="patient-123" />);
886
+
887
+ const heightCell = screen.getByRole('cell', { name: /182/ });
888
+ await user.hover(heightCell);
889
+ const editButton = within(heightCell).getByRole('button', { name: 'Edit' });
890
+ await user.click(editButton);
891
+
892
+ await waitFor(() => {
893
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
894
+ });
895
+
896
+ const input = screen.getByRole('spinbutton');
897
+ await user.clear(input);
898
+ await user.type(input, '185');
899
+
900
+ const saveButton = screen.getByRole('button', { name: 'Save' });
901
+ await user.click(saveButton);
902
+
903
+ await waitFor(() => {
904
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
905
+ expect.objectContaining({
906
+ kind: 'error',
907
+ title: 'Error saving observation',
908
+ }),
909
+ );
910
+ });
911
+ });
912
+ });