@openmrs/esm-patient-task-list-app 12.1.1-pre.10907
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/.turbo/turbo-build.log +7 -0
- package/README.md +28 -0
- package/dist/105.js +1 -0
- package/dist/105.js.map +1 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/149.js +1 -0
- package/dist/149.js.map +1 -0
- package/dist/229.js +43 -0
- package/dist/229.js.map +1 -0
- package/dist/304.js +1 -0
- package/dist/304.js.map +1 -0
- package/dist/339.js +1 -0
- package/dist/339.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/396.js +1 -0
- package/dist/396.js.map +1 -0
- package/dist/409.js +6 -0
- package/dist/409.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/66.js +1 -0
- package/dist/66.js.map +1 -0
- package/dist/697.js +1 -0
- package/dist/697.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/720.js +1 -0
- package/dist/720.js.map +1 -0
- package/dist/752.js +1 -0
- package/dist/752.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/789.js +1 -0
- package/dist/789.js.map +1 -0
- package/dist/989.js +1 -0
- package/dist/989.js.map +1 -0
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-patient-task-list-app.js +6 -0
- package/dist/openmrs-esm-patient-task-list-app.js.buildmanifest.json +651 -0
- package/dist/openmrs-esm-patient-task-list-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +61 -0
- package/rspack.config.js +1 -0
- package/src/config-schema.ts +13 -0
- package/src/declarations.d.ts +3 -0
- package/src/index.ts +25 -0
- package/src/launch-button/task-list-launch-button.extension.tsx +20 -0
- package/src/loader/loader.component.tsx +10 -0
- package/src/loader/loader.scss +9 -0
- package/src/routes.json +28 -0
- package/src/types.d.ts +9 -0
- package/src/workspace/add-task-form.component.tsx +609 -0
- package/src/workspace/add-task-form.scss +49 -0
- package/src/workspace/add-task-form.test.tsx +615 -0
- package/src/workspace/delete-task.modal.test.tsx +99 -0
- package/src/workspace/delete-task.modal.tsx +71 -0
- package/src/workspace/delete-task.scss +7 -0
- package/src/workspace/task-details-view.component.tsx +212 -0
- package/src/workspace/task-details-view.scss +61 -0
- package/src/workspace/task-details-view.test.tsx +408 -0
- package/src/workspace/task-list-view.component.tsx +154 -0
- package/src/workspace/task-list-view.scss +111 -0
- package/src/workspace/task-list-view.test.tsx +246 -0
- package/src/workspace/task-list.resource.ts +543 -0
- package/src/workspace/task-list.scss +37 -0
- package/src/workspace/task-list.workspace.test.tsx +135 -0
- package/src/workspace/task-list.workspace.tsx +99 -0
- package/translations/en.json +66 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import {
|
|
5
|
+
getDefaultsFromConfigSchema,
|
|
6
|
+
showSnackbar,
|
|
7
|
+
useConfig,
|
|
8
|
+
useLayoutType,
|
|
9
|
+
type Visit,
|
|
10
|
+
} from '@openmrs/esm-framework';
|
|
11
|
+
import AddTaskForm from './add-task-form.component';
|
|
12
|
+
import { useTask, saveTask, updateTask, useFetchProviders, useProviderRoles, type Task } from './task-list.resource';
|
|
13
|
+
import { configSchema, type Config } from '../config-schema';
|
|
14
|
+
|
|
15
|
+
const emptySystemTasks: never[] = [];
|
|
16
|
+
jest.mock('./task-list.resource', () => ({
|
|
17
|
+
useTask: jest.fn(),
|
|
18
|
+
saveTask: jest.fn(),
|
|
19
|
+
updateTask: jest.fn(),
|
|
20
|
+
taskListSWRKey: jest.fn((patientUuid) => `tasks-${patientUuid}`),
|
|
21
|
+
useFetchProviders: jest.fn(),
|
|
22
|
+
useProviderRoles: jest.fn(),
|
|
23
|
+
getPriorityLabel: jest.fn((priority) => priority),
|
|
24
|
+
useSystemTasks: jest.fn(() => ({ systemTasks: emptySystemTasks, isLoading: false })),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const mockUseTask = jest.mocked(useTask);
|
|
28
|
+
const mockSaveTask = jest.mocked(saveTask);
|
|
29
|
+
const mockUpdateTask = jest.mocked(updateTask);
|
|
30
|
+
const mockUseFetchProviders = jest.mocked(useFetchProviders);
|
|
31
|
+
const mockUseProviderRoles = jest.mocked(useProviderRoles);
|
|
32
|
+
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
33
|
+
const mockUseConfig = jest.mocked(useConfig<Config>);
|
|
34
|
+
|
|
35
|
+
const mockActiveVisit = { uuid: 'active-visit-uuid' } as Visit;
|
|
36
|
+
|
|
37
|
+
describe('AddTaskForm', () => {
|
|
38
|
+
const patientUuid = 'patient-uuid-123';
|
|
39
|
+
const mockOnClose = jest.fn();
|
|
40
|
+
|
|
41
|
+
const baseTask: Task = {
|
|
42
|
+
uuid: 'task-uuid-456',
|
|
43
|
+
name: 'Existing Task',
|
|
44
|
+
status: 'not-started',
|
|
45
|
+
createdDate: new Date('2024-01-15T10:00:00Z'),
|
|
46
|
+
completed: false,
|
|
47
|
+
createdBy: 'Test User',
|
|
48
|
+
rationale: 'Test rationale',
|
|
49
|
+
priority: 'high',
|
|
50
|
+
dueDate: {
|
|
51
|
+
type: 'DATE',
|
|
52
|
+
date: new Date('2024-01-20T10:00:00Z'),
|
|
53
|
+
},
|
|
54
|
+
assignee: {
|
|
55
|
+
uuid: 'provider-uuid-789',
|
|
56
|
+
display: 'Dr. Test Provider',
|
|
57
|
+
type: 'person',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
mockUseConfig.mockReturnValue({
|
|
63
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Default mocks
|
|
67
|
+
mockUseTask.mockReturnValue({
|
|
68
|
+
task: null,
|
|
69
|
+
isLoading: false,
|
|
70
|
+
error: null,
|
|
71
|
+
mutate: jest.fn(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
mockUseFetchProviders.mockReturnValue({
|
|
75
|
+
providers: [],
|
|
76
|
+
setProviderQuery: jest.fn(),
|
|
77
|
+
isLoading: false,
|
|
78
|
+
error: null,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
mockUseProviderRoles.mockReturnValue([]);
|
|
82
|
+
|
|
83
|
+
mockSaveTask.mockResolvedValue({} as any);
|
|
84
|
+
mockUpdateTask.mockResolvedValue({} as any);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('Create mode (no editTaskUuid)', () => {
|
|
88
|
+
it('should render the form with empty fields', () => {
|
|
89
|
+
render(<AddTaskForm patientUuid={patientUuid} activeVisit={mockActiveVisit} onClose={mockOnClose} />);
|
|
90
|
+
|
|
91
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('');
|
|
92
|
+
expect(screen.getByText(/add task/i)).toBeInTheDocument();
|
|
93
|
+
expect(screen.getByText(/discard/i)).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should call saveTask when submitting in create mode', async () => {
|
|
97
|
+
const user = userEvent.setup();
|
|
98
|
+
|
|
99
|
+
render(<AddTaskForm patientUuid={patientUuid} activeVisit={mockActiveVisit} onClose={mockOnClose} />);
|
|
100
|
+
|
|
101
|
+
const taskNameInput = screen.getByLabelText(/task name/i);
|
|
102
|
+
await user.type(taskNameInput, 'New Task');
|
|
103
|
+
|
|
104
|
+
const addButton = screen.getByRole('button', { name: /add task/i });
|
|
105
|
+
await user.click(addButton);
|
|
106
|
+
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(mockSaveTask).toHaveBeenCalledWith(
|
|
109
|
+
patientUuid,
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
name: 'New Task',
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(mockUpdateTask).not.toHaveBeenCalled();
|
|
117
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
118
|
+
expect.objectContaining({
|
|
119
|
+
title: 'Task added',
|
|
120
|
+
kind: 'success',
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
expect(mockOnClose).toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should default to "None" due date and submit without a dueDate', async () => {
|
|
127
|
+
const user = userEvent.setup();
|
|
128
|
+
|
|
129
|
+
render(<AddTaskForm patientUuid={patientUuid} activeVisit={mockActiveVisit} onClose={mockOnClose} />);
|
|
130
|
+
|
|
131
|
+
// "None" tab should be selected by default
|
|
132
|
+
const noneTab = screen.getByRole('tab', { name: /^none$/i });
|
|
133
|
+
expect(noneTab).toHaveAttribute('aria-selected', 'true');
|
|
134
|
+
|
|
135
|
+
const taskNameInput = screen.getByLabelText(/task name/i);
|
|
136
|
+
await user.type(taskNameInput, 'Task Without Due Date');
|
|
137
|
+
|
|
138
|
+
const addButton = screen.getByRole('button', { name: /add task/i });
|
|
139
|
+
await user.click(addButton);
|
|
140
|
+
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(mockSaveTask).toHaveBeenCalledWith(
|
|
143
|
+
patientUuid,
|
|
144
|
+
expect.objectContaining({
|
|
145
|
+
name: 'Task Without Due Date',
|
|
146
|
+
dueDate: undefined,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should select "None" when editing a task with no due date', async () => {
|
|
153
|
+
const taskWithNoDueDate: Task = {
|
|
154
|
+
...baseTask,
|
|
155
|
+
dueDate: undefined,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
mockUseTask.mockReturnValue({
|
|
159
|
+
task: taskWithNoDueDate,
|
|
160
|
+
isLoading: false,
|
|
161
|
+
error: null,
|
|
162
|
+
mutate: jest.fn(),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
render(
|
|
166
|
+
<AddTaskForm
|
|
167
|
+
patientUuid={patientUuid}
|
|
168
|
+
activeVisit={mockActiveVisit}
|
|
169
|
+
onClose={mockOnClose}
|
|
170
|
+
editTaskUuid="task-uuid-456"
|
|
171
|
+
/>,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
await waitFor(() => {
|
|
175
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const noneTab = screen.getByRole('tab', { name: /^none$/i });
|
|
179
|
+
expect(noneTab).toHaveAttribute('aria-selected', 'true');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should show error snackbar when saveTask fails', async () => {
|
|
183
|
+
const user = userEvent.setup();
|
|
184
|
+
mockSaveTask.mockRejectedValue(new Error('Network error'));
|
|
185
|
+
|
|
186
|
+
render(<AddTaskForm patientUuid={patientUuid} activeVisit={mockActiveVisit} onClose={mockOnClose} />);
|
|
187
|
+
|
|
188
|
+
const taskNameInput = screen.getByLabelText(/task name/i);
|
|
189
|
+
await user.type(taskNameInput, 'New Task');
|
|
190
|
+
|
|
191
|
+
const addButton = screen.getByRole('button', { name: /add task/i });
|
|
192
|
+
await user.click(addButton);
|
|
193
|
+
|
|
194
|
+
await waitFor(() => {
|
|
195
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
196
|
+
expect.objectContaining({
|
|
197
|
+
title: 'Task add failed',
|
|
198
|
+
kind: 'error',
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(mockOnClose).not.toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('Edit mode (with editTaskUuid)', () => {
|
|
208
|
+
const editTaskUuid = 'task-uuid-456';
|
|
209
|
+
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
mockUseTask.mockReturnValue({
|
|
212
|
+
task: baseTask,
|
|
213
|
+
isLoading: false,
|
|
214
|
+
error: null,
|
|
215
|
+
mutate: jest.fn(),
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should render the form with "Save task" and "Cancel" buttons', async () => {
|
|
220
|
+
render(
|
|
221
|
+
<AddTaskForm
|
|
222
|
+
patientUuid={patientUuid}
|
|
223
|
+
activeVisit={mockActiveVisit}
|
|
224
|
+
onClose={mockOnClose}
|
|
225
|
+
editTaskUuid={editTaskUuid}
|
|
226
|
+
/>,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await waitFor(() => {
|
|
230
|
+
expect(screen.getByRole('button', { name: /save task/i })).toBeInTheDocument();
|
|
231
|
+
});
|
|
232
|
+
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
|
233
|
+
expect(screen.queryByText(/add task/i)).not.toBeInTheDocument();
|
|
234
|
+
expect(screen.queryByText(/discard/i)).not.toBeInTheDocument();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should pre-populate form fields with existing task data', async () => {
|
|
238
|
+
render(
|
|
239
|
+
<AddTaskForm
|
|
240
|
+
patientUuid={patientUuid}
|
|
241
|
+
activeVisit={mockActiveVisit}
|
|
242
|
+
onClose={mockOnClose}
|
|
243
|
+
editTaskUuid={editTaskUuid}
|
|
244
|
+
/>,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
await waitFor(() => {
|
|
248
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Check rationale is populated
|
|
252
|
+
const rationaleTextarea = screen.getByPlaceholderText(/add a note here/i);
|
|
253
|
+
expect(rationaleTextarea).toHaveValue('Test rationale');
|
|
254
|
+
|
|
255
|
+
// Check due date picker is shown (DATE type shows the date input)
|
|
256
|
+
// Wait for the date picker to render (react-hook-form reset + watch timing)
|
|
257
|
+
await waitFor(() => {
|
|
258
|
+
expect(screen.getByLabelText(/due date/i)).toBeInTheDocument();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Check priority is shown in the combobox input
|
|
262
|
+
const priorityInput = screen.getByRole('combobox', { name: /priority/i });
|
|
263
|
+
expect(priorityInput).toHaveValue('high');
|
|
264
|
+
|
|
265
|
+
// Check assignee is shown in the combobox input
|
|
266
|
+
const assigneeInput = screen.getByRole('combobox', { name: /^assign to provider$/i });
|
|
267
|
+
expect(assigneeInput).toHaveValue('Dr. Test Provider');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should call updateTask when submitting in edit mode', async () => {
|
|
271
|
+
const user = userEvent.setup();
|
|
272
|
+
|
|
273
|
+
render(
|
|
274
|
+
<AddTaskForm
|
|
275
|
+
patientUuid={patientUuid}
|
|
276
|
+
activeVisit={mockActiveVisit}
|
|
277
|
+
onClose={mockOnClose}
|
|
278
|
+
editTaskUuid={editTaskUuid}
|
|
279
|
+
/>,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Wait for form to be populated
|
|
283
|
+
await waitFor(() => {
|
|
284
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Modify the task name
|
|
288
|
+
const taskNameInput = screen.getByLabelText(/task name/i);
|
|
289
|
+
await user.clear(taskNameInput);
|
|
290
|
+
await user.type(taskNameInput, 'Updated Task Name');
|
|
291
|
+
|
|
292
|
+
const saveButton = screen.getByRole('button', { name: /save task/i });
|
|
293
|
+
await user.click(saveButton);
|
|
294
|
+
|
|
295
|
+
await waitFor(() => {
|
|
296
|
+
expect(mockUpdateTask).toHaveBeenCalledWith(
|
|
297
|
+
patientUuid,
|
|
298
|
+
expect.objectContaining({
|
|
299
|
+
uuid: editTaskUuid,
|
|
300
|
+
name: 'Updated Task Name',
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(mockSaveTask).not.toHaveBeenCalled();
|
|
306
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
title: 'Task updated',
|
|
309
|
+
kind: 'success',
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
expect(mockOnClose).toHaveBeenCalled();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should show error snackbar when updateTask fails', async () => {
|
|
316
|
+
const user = userEvent.setup();
|
|
317
|
+
mockUpdateTask.mockRejectedValue(new Error('Network error'));
|
|
318
|
+
|
|
319
|
+
render(
|
|
320
|
+
<AddTaskForm
|
|
321
|
+
patientUuid={patientUuid}
|
|
322
|
+
activeVisit={mockActiveVisit}
|
|
323
|
+
onClose={mockOnClose}
|
|
324
|
+
editTaskUuid={editTaskUuid}
|
|
325
|
+
/>,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Wait for form to be populated
|
|
329
|
+
await waitFor(() => {
|
|
330
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const saveButton = screen.getByRole('button', { name: /save task/i });
|
|
334
|
+
await user.click(saveButton);
|
|
335
|
+
|
|
336
|
+
await waitFor(() => {
|
|
337
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
338
|
+
expect.objectContaining({
|
|
339
|
+
title: 'Unable to update task',
|
|
340
|
+
kind: 'error',
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(mockOnClose).not.toHaveBeenCalled();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should preserve existing task properties when updating', async () => {
|
|
349
|
+
const user = userEvent.setup();
|
|
350
|
+
|
|
351
|
+
render(
|
|
352
|
+
<AddTaskForm
|
|
353
|
+
patientUuid={patientUuid}
|
|
354
|
+
activeVisit={mockActiveVisit}
|
|
355
|
+
onClose={mockOnClose}
|
|
356
|
+
editTaskUuid={editTaskUuid}
|
|
357
|
+
/>,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Wait for form to be populated
|
|
361
|
+
await waitFor(() => {
|
|
362
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const saveButton = screen.getByRole('button', { name: /save task/i });
|
|
366
|
+
await user.click(saveButton);
|
|
367
|
+
|
|
368
|
+
await waitFor(() => {
|
|
369
|
+
expect(mockUpdateTask).toHaveBeenCalledWith(
|
|
370
|
+
patientUuid,
|
|
371
|
+
expect.objectContaining({
|
|
372
|
+
uuid: baseTask.uuid,
|
|
373
|
+
status: baseTask.status,
|
|
374
|
+
createdDate: baseTask.createdDate,
|
|
375
|
+
completed: baseTask.completed,
|
|
376
|
+
createdBy: baseTask.createdBy,
|
|
377
|
+
priority: baseTask.priority,
|
|
378
|
+
assignee: expect.objectContaining({
|
|
379
|
+
uuid: baseTask.assignee.uuid,
|
|
380
|
+
type: 'person',
|
|
381
|
+
}),
|
|
382
|
+
dueDate: expect.objectContaining({
|
|
383
|
+
type: 'DATE',
|
|
384
|
+
}),
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('Edit mode with different due date types', () => {
|
|
392
|
+
it('should select DATE tab and show date picker when editing task with DATE due date', async () => {
|
|
393
|
+
mockUseTask.mockReturnValue({
|
|
394
|
+
task: baseTask, // baseTask has dueDate.type = 'DATE'
|
|
395
|
+
isLoading: false,
|
|
396
|
+
error: null,
|
|
397
|
+
mutate: jest.fn(),
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
render(
|
|
401
|
+
<AddTaskForm
|
|
402
|
+
patientUuid={patientUuid}
|
|
403
|
+
activeVisit={mockActiveVisit}
|
|
404
|
+
onClose={mockOnClose}
|
|
405
|
+
editTaskUuid="task-uuid-456"
|
|
406
|
+
/>,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
await waitFor(() => {
|
|
410
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Verify DATE tab is selected
|
|
414
|
+
const dateTab = screen.getByRole('tab', { name: /^date$/i });
|
|
415
|
+
expect(dateTab).toHaveAttribute('aria-selected', 'true');
|
|
416
|
+
|
|
417
|
+
// Verify other tabs are not selected
|
|
418
|
+
const thisVisitTab = screen.getByRole('tab', { name: /this visit/i });
|
|
419
|
+
const nextVisitTab = screen.getByRole('tab', { name: /next visit/i });
|
|
420
|
+
expect(thisVisitTab).toHaveAttribute('aria-selected', 'false');
|
|
421
|
+
expect(nextVisitTab).toHaveAttribute('aria-selected', 'false');
|
|
422
|
+
|
|
423
|
+
// Verify date input is shown
|
|
424
|
+
// Wait for the date picker to render (react-hook-form reset + watch timing)
|
|
425
|
+
await waitFor(() => {
|
|
426
|
+
expect(screen.getByLabelText(/due date/i)).toBeInTheDocument();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should select THIS_VISIT tab when editing task with THIS_VISIT due date', async () => {
|
|
431
|
+
const taskWithThisVisit: Task = {
|
|
432
|
+
...baseTask,
|
|
433
|
+
dueDate: {
|
|
434
|
+
type: 'THIS_VISIT',
|
|
435
|
+
referenceVisitUuid: 'visit-uuid-123',
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
mockUseTask.mockReturnValue({
|
|
440
|
+
task: taskWithThisVisit,
|
|
441
|
+
isLoading: false,
|
|
442
|
+
error: null,
|
|
443
|
+
mutate: jest.fn(),
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
render(
|
|
447
|
+
<AddTaskForm
|
|
448
|
+
patientUuid={patientUuid}
|
|
449
|
+
activeVisit={mockActiveVisit}
|
|
450
|
+
onClose={mockOnClose}
|
|
451
|
+
editTaskUuid="task-uuid-456"
|
|
452
|
+
/>,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
await waitFor(() => {
|
|
456
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Verify THIS_VISIT tab is selected
|
|
460
|
+
const thisVisitTab = screen.getByRole('tab', { name: /this visit/i });
|
|
461
|
+
expect(thisVisitTab).toHaveAttribute('aria-selected', 'true');
|
|
462
|
+
|
|
463
|
+
// Verify other tabs are not selected
|
|
464
|
+
const dateTab = screen.getByRole('tab', { name: /^date$/i });
|
|
465
|
+
const nextVisitTab = screen.getByRole('tab', { name: /next visit/i });
|
|
466
|
+
expect(dateTab).toHaveAttribute('aria-selected', 'false');
|
|
467
|
+
expect(nextVisitTab).toHaveAttribute('aria-selected', 'false');
|
|
468
|
+
|
|
469
|
+
// Verify date input is NOT shown (visit-based due dates don't show date picker)
|
|
470
|
+
expect(screen.queryByDisplayValue('20/01/2024')).not.toBeInTheDocument();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('should select NEXT_VISIT tab when editing task with NEXT_VISIT due date', async () => {
|
|
474
|
+
const taskWithNextVisit: Task = {
|
|
475
|
+
...baseTask,
|
|
476
|
+
dueDate: {
|
|
477
|
+
type: 'NEXT_VISIT',
|
|
478
|
+
referenceVisitUuid: 'visit-uuid-456',
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
mockUseTask.mockReturnValue({
|
|
483
|
+
task: taskWithNextVisit,
|
|
484
|
+
isLoading: false,
|
|
485
|
+
error: null,
|
|
486
|
+
mutate: jest.fn(),
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
render(
|
|
490
|
+
<AddTaskForm
|
|
491
|
+
patientUuid={patientUuid}
|
|
492
|
+
activeVisit={mockActiveVisit}
|
|
493
|
+
onClose={mockOnClose}
|
|
494
|
+
editTaskUuid="task-uuid-456"
|
|
495
|
+
/>,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
await waitFor(() => {
|
|
499
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Verify NEXT_VISIT tab is selected
|
|
503
|
+
const nextVisitTab = screen.getByRole('tab', { name: /next visit/i });
|
|
504
|
+
expect(nextVisitTab).toHaveAttribute('aria-selected', 'true');
|
|
505
|
+
|
|
506
|
+
// Verify other tabs are not selected
|
|
507
|
+
const dateTab = screen.getByRole('tab', { name: /^date$/i });
|
|
508
|
+
const thisVisitTab = screen.getByRole('tab', { name: /this visit/i });
|
|
509
|
+
expect(dateTab).toHaveAttribute('aria-selected', 'false');
|
|
510
|
+
expect(thisVisitTab).toHaveAttribute('aria-selected', 'false');
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should preserve referenceVisitUuid when editing a visit-based task', async () => {
|
|
514
|
+
const user = userEvent.setup();
|
|
515
|
+
const taskWithThisVisit: Task = {
|
|
516
|
+
...baseTask,
|
|
517
|
+
dueDate: {
|
|
518
|
+
type: 'THIS_VISIT',
|
|
519
|
+
referenceVisitUuid: 'original-visit-uuid-999',
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
mockUseTask.mockReturnValue({
|
|
524
|
+
task: taskWithThisVisit,
|
|
525
|
+
isLoading: false,
|
|
526
|
+
error: null,
|
|
527
|
+
mutate: jest.fn(),
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
render(
|
|
531
|
+
<AddTaskForm
|
|
532
|
+
patientUuid={patientUuid}
|
|
533
|
+
activeVisit={mockActiveVisit}
|
|
534
|
+
onClose={mockOnClose}
|
|
535
|
+
editTaskUuid="task-uuid-456"
|
|
536
|
+
/>,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
await waitFor(() => {
|
|
540
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const saveButton = screen.getByRole('button', { name: /save task/i });
|
|
544
|
+
await user.click(saveButton);
|
|
545
|
+
|
|
546
|
+
await waitFor(() => {
|
|
547
|
+
expect(mockUpdateTask).toHaveBeenCalledWith(
|
|
548
|
+
patientUuid,
|
|
549
|
+
expect.objectContaining({
|
|
550
|
+
dueDate: expect.objectContaining({
|
|
551
|
+
type: 'THIS_VISIT',
|
|
552
|
+
referenceVisitUuid: 'original-visit-uuid-999',
|
|
553
|
+
}),
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should send provider role assignee when updating task with role assignment', async () => {
|
|
560
|
+
const user = userEvent.setup();
|
|
561
|
+
const taskWithRoleAssignee: Task = {
|
|
562
|
+
...baseTask,
|
|
563
|
+
assignee: {
|
|
564
|
+
uuid: 'role-uuid-123',
|
|
565
|
+
display: 'Nurse Role',
|
|
566
|
+
type: 'role',
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
mockUseTask.mockReturnValue({
|
|
571
|
+
task: taskWithRoleAssignee,
|
|
572
|
+
isLoading: false,
|
|
573
|
+
error: null,
|
|
574
|
+
mutate: jest.fn(),
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
mockUseConfig.mockReturnValue({
|
|
578
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
579
|
+
allowAssigningProviderRole: true,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
render(
|
|
583
|
+
<AddTaskForm
|
|
584
|
+
patientUuid={patientUuid}
|
|
585
|
+
activeVisit={mockActiveVisit}
|
|
586
|
+
onClose={mockOnClose}
|
|
587
|
+
editTaskUuid="task-uuid-456"
|
|
588
|
+
/>,
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
await waitFor(() => {
|
|
592
|
+
expect(screen.getByLabelText(/task name/i)).toHaveValue('Existing Task');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Verify the role is displayed in the provider role combobox
|
|
596
|
+
const roleInput = screen.getByRole('combobox', { name: /assign to provider role/i });
|
|
597
|
+
expect(roleInput).toHaveValue('Nurse Role');
|
|
598
|
+
|
|
599
|
+
const saveButton = screen.getByRole('button', { name: /save task/i });
|
|
600
|
+
await user.click(saveButton);
|
|
601
|
+
|
|
602
|
+
await waitFor(() => {
|
|
603
|
+
expect(mockUpdateTask).toHaveBeenCalledWith(
|
|
604
|
+
patientUuid,
|
|
605
|
+
expect.objectContaining({
|
|
606
|
+
assignee: expect.objectContaining({
|
|
607
|
+
uuid: 'role-uuid-123',
|
|
608
|
+
type: 'role',
|
|
609
|
+
}),
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { getCoreTranslation, showSnackbar } from '@openmrs/esm-framework';
|
|
5
|
+
import { deleteTask, type Task } from './task-list.resource';
|
|
6
|
+
import DeleteTaskModal from './delete-task.modal';
|
|
7
|
+
|
|
8
|
+
const mockDeleteTask = jest.mocked(deleteTask);
|
|
9
|
+
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
10
|
+
|
|
11
|
+
jest.mock('./task-list.resource', () => ({
|
|
12
|
+
deleteTask: jest.fn(),
|
|
13
|
+
taskListSWRKey: jest.fn((patientUuid) => `tasks-${patientUuid}`),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const baseTask: Task = {
|
|
17
|
+
uuid: 'task-uuid-123',
|
|
18
|
+
name: 'Test Task',
|
|
19
|
+
status: 'not-started',
|
|
20
|
+
createdDate: new Date('2024-01-15T10:00:00Z'),
|
|
21
|
+
completed: false,
|
|
22
|
+
createdBy: 'Test User',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const defaultProps = {
|
|
26
|
+
closeModal: jest.fn(),
|
|
27
|
+
task: baseTask,
|
|
28
|
+
patientUuid: 'patient-uuid-123',
|
|
29
|
+
onDeleted: jest.fn(),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('DeleteTaskModal', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders the modal with correct elements', () => {
|
|
38
|
+
render(<DeleteTaskModal {...defaultProps} />);
|
|
39
|
+
|
|
40
|
+
expect(screen.getByRole('heading', { name: /delete task/i })).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText(/are you sure you want to delete this task/i)).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByRole('button', { name: new RegExp(getCoreTranslation('cancel'), 'i') })).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByRole('button', { name: new RegExp(getCoreTranslation('delete'), 'i') })).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('calls closeModal when Cancel button is clicked', async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
|
|
49
|
+
render(<DeleteTaskModal {...defaultProps} />);
|
|
50
|
+
|
|
51
|
+
const cancelButton = screen.getByRole('button', { name: new RegExp(getCoreTranslation('cancel'), 'i') });
|
|
52
|
+
await user.click(cancelButton);
|
|
53
|
+
|
|
54
|
+
expect(defaultProps.closeModal).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('deletes the task and shows success snackbar', async () => {
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
mockDeleteTask.mockResolvedValue({} as any);
|
|
60
|
+
|
|
61
|
+
render(<DeleteTaskModal {...defaultProps} />);
|
|
62
|
+
|
|
63
|
+
const deleteButton = screen.getByRole('button', { name: new RegExp(getCoreTranslation('delete'), 'i') });
|
|
64
|
+
await user.click(deleteButton);
|
|
65
|
+
|
|
66
|
+
expect(mockDeleteTask).toHaveBeenCalledWith('patient-uuid-123', baseTask);
|
|
67
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
68
|
+
expect.objectContaining({
|
|
69
|
+
kind: 'success',
|
|
70
|
+
title: 'Task deleted',
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
expect(defaultProps.closeModal).toHaveBeenCalled();
|
|
74
|
+
expect(defaultProps.onDeleted).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('shows error snackbar when delete fails', async () => {
|
|
78
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
79
|
+
const user = userEvent.setup();
|
|
80
|
+
mockDeleteTask.mockRejectedValue(new Error('Network error'));
|
|
81
|
+
|
|
82
|
+
render(<DeleteTaskModal {...defaultProps} />);
|
|
83
|
+
|
|
84
|
+
const deleteButton = screen.getByRole('button', { name: new RegExp(getCoreTranslation('delete'), 'i') });
|
|
85
|
+
await user.click(deleteButton);
|
|
86
|
+
|
|
87
|
+
expect(mockDeleteTask).toHaveBeenCalledWith('patient-uuid-123', baseTask);
|
|
88
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
kind: 'error',
|
|
91
|
+
title: 'Error deleting task',
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
expect(defaultProps.closeModal).not.toHaveBeenCalled();
|
|
95
|
+
expect(defaultProps.onDeleted).not.toHaveBeenCalled();
|
|
96
|
+
|
|
97
|
+
consoleSpy.mockRestore();
|
|
98
|
+
});
|
|
99
|
+
});
|