@openmrs/esm-patient-tests-app 11.3.1-pre.9433 → 11.3.1-pre.9437

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 (46) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/dist/1477.js +1 -1
  3. package/dist/1477.js.map +1 -1
  4. package/dist/1935.js +1 -1
  5. package/dist/1935.js.map +1 -1
  6. package/dist/3509.js +1 -1
  7. package/dist/3509.js.map +1 -1
  8. package/dist/4300.js +1 -1
  9. package/dist/6301.js +1 -1
  10. package/dist/6301.js.map +1 -1
  11. package/dist/7202.js +1 -1
  12. package/dist/7202.js.map +1 -1
  13. package/dist/8555.js +1 -1
  14. package/dist/8555.js.map +1 -1
  15. package/dist/main.js +1 -1
  16. package/dist/main.js.map +1 -1
  17. package/dist/openmrs-esm-patient-tests-app.js +1 -1
  18. package/dist/openmrs-esm-patient-tests-app.js.buildmanifest.json +19 -19
  19. package/dist/routes.json +1 -1
  20. package/package.json +2 -2
  21. package/src/index.ts +1 -1
  22. package/src/routes.json +1 -1
  23. package/src/test-orders/add-test-order/add-test-order.test.tsx +1 -1
  24. package/src/test-orders/add-test-order/add-test-order.workspace.tsx +1 -1
  25. package/src/test-orders/add-test-order/test-order-form.component.tsx +1 -1
  26. package/src/test-orders/add-test-order/test-type-search.component.tsx +1 -1
  27. package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx +1 -1
  28. package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.test.tsx +2 -2
  29. package/src/test-results/filter/filter-context.test.tsx +556 -0
  30. package/src/test-results/filter/filter-context.tsx +1 -1
  31. package/src/test-results/filter/filter-reducer.test.ts +540 -0
  32. package/src/test-results/filter/filter-reducer.ts +1 -1
  33. package/src/test-results/filter/filter-set.test.tsx +694 -0
  34. package/src/test-results/grouped-timeline/grouped-timeline.test.tsx +1 -1
  35. package/src/test-results/grouped-timeline/useObstreeData.test.ts +471 -0
  36. package/src/test-results/individual-results-table-tablet/usePanelData.tsx +40 -26
  37. package/src/test-results/loadPatientTestData/helpers.ts +29 -12
  38. package/src/test-results/loadPatientTestData/usePatientResultsData.ts +18 -7
  39. package/src/test-results/overview/external-overview.extension.tsx +1 -2
  40. package/src/test-results/print-modal/print-modal.extension.tsx +1 -1
  41. package/src/test-results/results-viewer/results-viewer.extension.tsx +7 -3
  42. package/src/test-results/tree-view/tree-view.component.tsx +6 -1
  43. package/src/test-results/tree-view/tree-view.test.tsx +117 -1
  44. package/src/test-results/trendline/trendline.component.tsx +88 -52
  45. package/src/test-results/ui-elements/reset-filters-empty-state/filter-empty-data-illustration.tsx +2 -2
  46. package/translations/en.json +1 -1
@@ -0,0 +1,556 @@
1
+ import React, { useContext } from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { FilterProvider, FilterContext } from './filter-context';
5
+ import { type TreeNode } from './filter-types';
6
+
7
+ // Test component to access context values
8
+ const TestConsumer = () => {
9
+ const {
10
+ activeTests,
11
+ someChecked,
12
+ totalResultsCount,
13
+ filteredResultsCount,
14
+ timelineData,
15
+ tableData,
16
+ toggleVal,
17
+ updateParent,
18
+ resetTree,
19
+ } = useContext(FilterContext);
20
+
21
+ return (
22
+ <div>
23
+ <div data-testid="active-tests">{activeTests.join(',')}</div>
24
+ <div data-testid="some-checked">{someChecked.toString()}</div>
25
+ <div data-testid="total-count">{totalResultsCount}</div>
26
+ <div data-testid="filtered-count">{filteredResultsCount}</div>
27
+ <div data-testid="timeline-loaded">{timelineData?.loaded.toString()}</div>
28
+ <div data-testid="timeline-rows">{timelineData?.data?.rowData?.length || 0}</div>
29
+ <div data-testid="table-groups">{tableData?.length || 0}</div>
30
+ <button onClick={() => toggleVal('Test1')}>Toggle Test1</button>
31
+ <button onClick={() => updateParent('Panel1')}>Toggle Panel1</button>
32
+ <button onClick={resetTree}>Reset</button>
33
+ </div>
34
+ );
35
+ };
36
+
37
+ const mockRoots: Array<TreeNode> = [
38
+ {
39
+ display: 'Complete Blood Count',
40
+ flatName: 'CBC',
41
+ hasData: true,
42
+ subSets: [
43
+ {
44
+ display: 'Hemoglobin',
45
+ flatName: 'CBC: Hemoglobin',
46
+ hasData: true,
47
+ obs: [
48
+ {
49
+ obsDatetime: '2024-01-15T10:00:00.000Z',
50
+ value: '12.5',
51
+ interpretation: 'NORMAL',
52
+ },
53
+ {
54
+ obsDatetime: '2024-01-10T10:00:00.000Z',
55
+ value: '12.0',
56
+ interpretation: 'NORMAL',
57
+ },
58
+ ],
59
+ subSets: [],
60
+ },
61
+ {
62
+ display: 'White Blood Cell Count',
63
+ flatName: 'CBC: WBC',
64
+ hasData: true,
65
+ obs: [
66
+ {
67
+ obsDatetime: '2024-01-15T10:00:00.000Z',
68
+ value: '7.5',
69
+ interpretation: 'NORMAL',
70
+ },
71
+ ],
72
+ subSets: [],
73
+ },
74
+ ],
75
+ },
76
+ {
77
+ display: 'Lipid Panel',
78
+ flatName: 'Lipid',
79
+ hasData: true,
80
+ subSets: [
81
+ {
82
+ display: 'Total Cholesterol',
83
+ flatName: 'Lipid: Cholesterol',
84
+ hasData: true,
85
+ obs: [
86
+ {
87
+ obsDatetime: '2024-01-20T10:00:00.000Z',
88
+ value: '180',
89
+ interpretation: 'NORMAL',
90
+ },
91
+ ],
92
+ subSets: [],
93
+ },
94
+ ],
95
+ },
96
+ ];
97
+
98
+ describe('FilterContext', () => {
99
+ describe('Initialization', () => {
100
+ it('should initialize with roots data', async () => {
101
+ render(
102
+ <FilterProvider roots={mockRoots} isLoading={false}>
103
+ <TestConsumer />
104
+ </FilterProvider>,
105
+ );
106
+
107
+ await waitFor(() => {
108
+ expect(screen.getByTestId('timeline-loaded')).toHaveTextContent('true');
109
+ });
110
+ });
111
+
112
+ it('should compute totalResultsCount from all observations', async () => {
113
+ render(
114
+ <FilterProvider roots={mockRoots} isLoading={false}>
115
+ <TestConsumer />
116
+ </FilterProvider>,
117
+ );
118
+
119
+ await waitFor(() => {
120
+ // 2 hemoglobin + 1 WBC + 1 cholesterol = 4 total
121
+ expect(screen.getByTestId('total-count')).toHaveTextContent('4');
122
+ });
123
+ });
124
+
125
+ it('should show filteredResultsCount equal to totalResultsCount when no filters applied', async () => {
126
+ render(
127
+ <FilterProvider roots={mockRoots} isLoading={false}>
128
+ <TestConsumer />
129
+ </FilterProvider>,
130
+ );
131
+
132
+ await waitFor(() => {
133
+ expect(screen.getByTestId('filtered-count')).toHaveTextContent('4');
134
+ });
135
+ expect(screen.getByTestId('some-checked')).toHaveTextContent('false');
136
+ });
137
+ });
138
+
139
+ describe('Active tests tracking', () => {
140
+ it('should track activeTests when checkboxes are toggled', async () => {
141
+ const user = userEvent.setup();
142
+
143
+ render(
144
+ <FilterProvider roots={mockRoots} isLoading={false}>
145
+ <TestConsumer />
146
+ </FilterProvider>,
147
+ );
148
+
149
+ await waitFor(() => {
150
+ expect(screen.getByTestId('timeline-loaded')).toHaveTextContent('true');
151
+ });
152
+
153
+ const toggleButton = screen.getByRole('button', { name: /toggle test1/i });
154
+ await user.click(toggleButton);
155
+
156
+ expect(screen.getByTestId('active-tests')).toHaveTextContent('Test1');
157
+ expect(screen.getByTestId('some-checked')).toHaveTextContent('true');
158
+ });
159
+
160
+ it('should update someChecked when tests are selected', async () => {
161
+ const user = userEvent.setup();
162
+
163
+ render(
164
+ <FilterProvider roots={mockRoots} isLoading={false}>
165
+ <TestConsumer />
166
+ </FilterProvider>,
167
+ );
168
+
169
+ await waitFor(() => {
170
+ expect(screen.getByTestId('timeline-loaded')).toHaveTextContent('true');
171
+ });
172
+
173
+ expect(screen.getByTestId('some-checked')).toHaveTextContent('false');
174
+
175
+ await user.click(screen.getByRole('button', { name: /toggle test1/i }));
176
+
177
+ expect(screen.getByTestId('some-checked')).toHaveTextContent('true');
178
+ });
179
+ });
180
+
181
+ describe('Filtered results count', () => {
182
+ it('should update filteredResultsCount when filters are applied', async () => {
183
+ const user = userEvent.setup();
184
+
185
+ const FilterTestComponent = () => {
186
+ const { toggleVal, filteredResultsCount, totalResultsCount, someChecked } = useContext(FilterContext);
187
+
188
+ return (
189
+ <div>
190
+ <div data-testid="total-count">{totalResultsCount}</div>
191
+ <div data-testid="filtered-count">{filteredResultsCount}</div>
192
+ <div data-testid="some-checked">{someChecked.toString()}</div>
193
+ <button onClick={() => toggleVal('CBC: Hemoglobin')}>Toggle Hemoglobin</button>
194
+ </div>
195
+ );
196
+ };
197
+
198
+ render(
199
+ <FilterProvider roots={mockRoots} isLoading={false}>
200
+ <FilterTestComponent />
201
+ </FilterProvider>,
202
+ );
203
+
204
+ await waitFor(() => {
205
+ expect(screen.getByTestId('total-count')).toHaveTextContent('4');
206
+ });
207
+
208
+ expect(screen.getByTestId('filtered-count')).toHaveTextContent('4');
209
+ expect(screen.getByTestId('some-checked')).toHaveTextContent('false');
210
+
211
+ await user.click(screen.getByRole('button', { name: /toggle hemoglobin/i }));
212
+ await waitFor(() => {
213
+ expect(screen.getByTestId('filtered-count')).toHaveTextContent('2');
214
+ });
215
+ expect(screen.getByTestId('some-checked')).toHaveTextContent('true');
216
+ });
217
+ });
218
+
219
+ describe('Timeline data', () => {
220
+ it('should generate timeline data with all tests when no filters applied', async () => {
221
+ render(
222
+ <FilterProvider roots={mockRoots} isLoading={false}>
223
+ <TestConsumer />
224
+ </FilterProvider>,
225
+ );
226
+
227
+ await waitFor(() => {
228
+ expect(screen.getByTestId('timeline-loaded')).toHaveTextContent('true');
229
+ });
230
+ expect(screen.getByTestId('timeline-rows')).toHaveTextContent('3');
231
+ });
232
+
233
+ it('should filter timeline data when tests are selected', async () => {
234
+ const user = userEvent.setup();
235
+
236
+ const TimelineTestComponent = () => {
237
+ const { toggleVal, timelineData } = useContext(FilterContext);
238
+
239
+ return (
240
+ <div>
241
+ <div data-testid="timeline-rows">{timelineData?.data?.rowData?.length || 0}</div>
242
+ <button onClick={() => toggleVal('CBC: Hemoglobin')}>Toggle Hemoglobin</button>
243
+ </div>
244
+ );
245
+ };
246
+
247
+ render(
248
+ <FilterProvider roots={mockRoots} isLoading={false}>
249
+ <TimelineTestComponent />
250
+ </FilterProvider>,
251
+ );
252
+
253
+ await waitFor(() => {
254
+ expect(screen.getByTestId('timeline-rows')).toHaveTextContent('3');
255
+ });
256
+
257
+ await user.click(screen.getByRole('button', { name: /toggle hemoglobin/i }));
258
+
259
+ await waitFor(() => {
260
+ expect(screen.getByTestId('timeline-rows')).toHaveTextContent('1');
261
+ });
262
+ });
263
+
264
+ it('should sort timeline observations by date descending', async () => {
265
+ const TimelineDetailsComponent = () => {
266
+ const { timelineData } = useContext(FilterContext);
267
+
268
+ return (
269
+ <div>
270
+ <div data-testid="timeline-loaded">{timelineData?.loaded.toString()}</div>
271
+ {timelineData?.data?.parsedTime?.sortedTimes && (
272
+ <div data-testid="first-time">{timelineData.data.parsedTime.sortedTimes[0]}</div>
273
+ )}
274
+ </div>
275
+ );
276
+ };
277
+
278
+ render(
279
+ <FilterProvider roots={mockRoots} isLoading={false}>
280
+ <TimelineDetailsComponent />
281
+ </FilterProvider>,
282
+ );
283
+
284
+ await waitFor(() => {
285
+ expect(screen.getByTestId('timeline-loaded')).toHaveTextContent('true');
286
+ });
287
+
288
+ const firstTime = screen.getByTestId('first-time').textContent;
289
+ expect(firstTime).toContain('2024-01-20');
290
+ });
291
+ });
292
+
293
+ describe('Table data', () => {
294
+ it('should generate grouped table data', async () => {
295
+ render(
296
+ <FilterProvider roots={mockRoots} isLoading={false}>
297
+ <TestConsumer />
298
+ </FilterProvider>,
299
+ );
300
+
301
+ await waitFor(() => {
302
+ const tableGroups = screen.getByTestId('table-groups');
303
+ expect(parseInt(tableGroups.textContent || '0')).toBeGreaterThan(0);
304
+ });
305
+ });
306
+
307
+ it('should group observations by panel and date', async () => {
308
+ const TableTestComponent = () => {
309
+ const { tableData } = useContext(FilterContext);
310
+
311
+ return (
312
+ <div>
313
+ <div data-testid="table-groups">{tableData?.length || 0}</div>
314
+ {tableData?.map((group, index) => (
315
+ <div key={index} data-testid={`group-${index}`}>
316
+ {group.key} - {group.date} - {group.entries.length}
317
+ </div>
318
+ ))}
319
+ </div>
320
+ );
321
+ };
322
+
323
+ render(
324
+ <FilterProvider roots={mockRoots} isLoading={false}>
325
+ <TableTestComponent />
326
+ </FilterProvider>,
327
+ );
328
+
329
+ await waitFor(() => {
330
+ expect(screen.getByTestId('table-groups')).not.toHaveTextContent('0');
331
+ });
332
+
333
+ const groups = screen.getAllByTestId(/^group-/);
334
+ expect(groups.length).toBeGreaterThan(0);
335
+ });
336
+ });
337
+
338
+ describe('Actions', () => {
339
+ it('should toggle individual test via toggleVal', async () => {
340
+ const user = userEvent.setup();
341
+
342
+ const ActionTestComponent = () => {
343
+ const { toggleVal, checkboxes } = useContext(FilterContext);
344
+
345
+ return (
346
+ <div>
347
+ <div data-testid="checkbox-state">{checkboxes['CBC: Hemoglobin']?.toString() || 'false'}</div>
348
+ <button onClick={() => toggleVal('CBC: Hemoglobin')}>Toggle</button>
349
+ </div>
350
+ );
351
+ };
352
+
353
+ render(
354
+ <FilterProvider roots={mockRoots} isLoading={false}>
355
+ <ActionTestComponent />
356
+ </FilterProvider>,
357
+ );
358
+
359
+ await waitFor(() => {
360
+ expect(screen.getByTestId('checkbox-state')).toBeInTheDocument();
361
+ });
362
+
363
+ expect(screen.getByTestId('checkbox-state')).toHaveTextContent('false');
364
+
365
+ await user.click(screen.getByRole('button', { name: /toggle/i }));
366
+
367
+ await waitFor(() => {
368
+ expect(screen.getByTestId('checkbox-state')).toHaveTextContent('true');
369
+ });
370
+ });
371
+
372
+ it('should reset all filters via resetTree', async () => {
373
+ const user = userEvent.setup();
374
+
375
+ const ResetTestComponent = () => {
376
+ const { toggleVal, resetTree, checkboxes, someChecked } = useContext(FilterContext);
377
+
378
+ return (
379
+ <div>
380
+ <div data-testid="some-checked">{someChecked.toString()}</div>
381
+ <div data-testid="hemoglobin-state">{checkboxes['CBC: Hemoglobin']?.toString() || 'false'}</div>
382
+ <button onClick={() => toggleVal('CBC: Hemoglobin')}>Toggle Hemoglobin</button>
383
+ <button onClick={resetTree}>Reset</button>
384
+ </div>
385
+ );
386
+ };
387
+
388
+ render(
389
+ <FilterProvider roots={mockRoots} isLoading={false}>
390
+ <ResetTestComponent />
391
+ </FilterProvider>,
392
+ );
393
+
394
+ await waitFor(() => {
395
+ expect(screen.getByTestId('some-checked')).toBeInTheDocument();
396
+ });
397
+
398
+ await user.click(screen.getByRole('button', { name: /toggle hemoglobin/i }));
399
+
400
+ await waitFor(() => {
401
+ expect(screen.getByTestId('some-checked')).toHaveTextContent('true');
402
+ });
403
+ expect(screen.getByTestId('hemoglobin-state')).toHaveTextContent('true');
404
+
405
+ await user.click(screen.getByRole('button', { name: /reset/i }));
406
+
407
+ await waitFor(() => {
408
+ expect(screen.getByTestId('some-checked')).toHaveTextContent('false');
409
+ });
410
+ expect(screen.getByTestId('hemoglobin-state')).toHaveTextContent('false');
411
+ });
412
+ });
413
+
414
+ describe('Edge cases', () => {
415
+ it('should handle empty roots gracefully', () => {
416
+ render(
417
+ <FilterProvider roots={[]} isLoading={false}>
418
+ <TestConsumer />
419
+ </FilterProvider>,
420
+ );
421
+
422
+ expect(screen.getByTestId('total-count')).toHaveTextContent('0');
423
+ expect(screen.getByTestId('filtered-count')).toHaveTextContent('0');
424
+ });
425
+
426
+ it('should handle tests with no observations', async () => {
427
+ const emptyRoots: Array<TreeNode> = [
428
+ {
429
+ display: 'Empty Panel',
430
+ flatName: 'Empty',
431
+ hasData: false,
432
+ subSets: [
433
+ {
434
+ display: 'Empty Test',
435
+ flatName: 'Empty: Test',
436
+ hasData: false,
437
+ obs: [],
438
+ subSets: [],
439
+ },
440
+ ],
441
+ },
442
+ ];
443
+
444
+ render(
445
+ <FilterProvider roots={emptyRoots} isLoading={false}>
446
+ <TestConsumer />
447
+ </FilterProvider>,
448
+ );
449
+
450
+ await waitFor(() => {
451
+ expect(screen.getByTestId('total-count')).toHaveTextContent('0');
452
+ });
453
+ });
454
+
455
+ it('should reflect isLoading prop', () => {
456
+ const LoadingTestComponent = () => {
457
+ const { isLoading } = useContext(FilterContext);
458
+ return <div data-testid="is-loading">{isLoading.toString()}</div>;
459
+ };
460
+
461
+ const { rerender } = render(
462
+ <FilterProvider roots={mockRoots} isLoading={true}>
463
+ <LoadingTestComponent />
464
+ </FilterProvider>,
465
+ );
466
+
467
+ expect(screen.getByTestId('is-loading')).toHaveTextContent('true');
468
+
469
+ rerender(
470
+ <FilterProvider roots={mockRoots} isLoading={false}>
471
+ <LoadingTestComponent />
472
+ </FilterProvider>,
473
+ );
474
+
475
+ expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
476
+ });
477
+ });
478
+
479
+ describe('Computed values reactivity', () => {
480
+ it('should recompute timelineData when checkboxes change', async () => {
481
+ const user = userEvent.setup();
482
+
483
+ const ReactivityTestComponent = () => {
484
+ const { toggleVal, timelineData } = useContext(FilterContext);
485
+
486
+ return (
487
+ <div>
488
+ <div data-testid="row-count">{timelineData?.data?.rowData?.length || 0}</div>
489
+ <button onClick={() => toggleVal('CBC: Hemoglobin')}>Toggle Hemoglobin</button>
490
+ <button onClick={() => toggleVal('CBC: WBC')}>Toggle WBC</button>
491
+ </div>
492
+ );
493
+ };
494
+
495
+ render(
496
+ <FilterProvider roots={mockRoots} isLoading={false}>
497
+ <ReactivityTestComponent />
498
+ </FilterProvider>,
499
+ );
500
+
501
+ await waitFor(() => {
502
+ expect(screen.getByTestId('row-count')).toHaveTextContent('3');
503
+ });
504
+
505
+ await user.click(screen.getByRole('button', { name: /toggle hemoglobin/i }));
506
+
507
+ await waitFor(() => {
508
+ expect(screen.getByTestId('row-count')).toHaveTextContent('1');
509
+ });
510
+
511
+ await user.click(screen.getByRole('button', { name: /toggle wbc/i }));
512
+
513
+ await waitFor(() => {
514
+ expect(screen.getByTestId('row-count')).toHaveTextContent('2');
515
+ });
516
+ });
517
+
518
+ it('should recompute filteredResultsCount when selections change', async () => {
519
+ const user = userEvent.setup();
520
+
521
+ const CountTestComponent = () => {
522
+ const { toggleVal, filteredResultsCount } = useContext(FilterContext);
523
+
524
+ return (
525
+ <div>
526
+ <div data-testid="filtered-count">{filteredResultsCount}</div>
527
+ <button onClick={() => toggleVal('CBC: Hemoglobin')}>Toggle Hemoglobin</button>
528
+ <button onClick={() => toggleVal('Lipid: Cholesterol')}>Toggle Cholesterol</button>
529
+ </div>
530
+ );
531
+ };
532
+
533
+ render(
534
+ <FilterProvider roots={mockRoots} isLoading={false}>
535
+ <CountTestComponent />
536
+ </FilterProvider>,
537
+ );
538
+
539
+ await waitFor(() => {
540
+ expect(screen.getByTestId('filtered-count')).toHaveTextContent('4');
541
+ });
542
+
543
+ await user.click(screen.getByRole('button', { name: /toggle hemoglobin/i }));
544
+
545
+ await waitFor(() => {
546
+ expect(screen.getByTestId('filtered-count')).toHaveTextContent('2');
547
+ });
548
+
549
+ await user.click(screen.getByRole('button', { name: /toggle cholesterol/i }));
550
+
551
+ await waitFor(() => {
552
+ expect(screen.getByTestId('filtered-count')).toHaveTextContent('3');
553
+ });
554
+ });
555
+ });
556
+ });
@@ -195,7 +195,7 @@ const FilterProvider = ({ roots, isLoading, children }: FilterProviderProps) =>
195
195
  if (roots.length && !Object.keys(state.parents).length) {
196
196
  actions.initialize(roots);
197
197
  }
198
- }, [actions, state, roots]);
198
+ }, [actions, state.parents, roots]);
199
199
 
200
200
  const totalResultsCount: number = useMemo(() => {
201
201
  let count = 0;