@dhis2/analytics 28.1.2 → 29.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 (59) hide show
  1. package/build/cjs/__demo__/InterpretationsUnit.stories.js +9 -6
  2. package/build/cjs/__fixtures__/interpretationsMockData.js +204 -0
  3. package/build/cjs/components/Interpretations/DashboardItemInterpretations/DashboardInterpretationThread.js +56 -0
  4. package/build/cjs/components/Interpretations/DashboardItemInterpretations/DashboardItemInterpretations.js +54 -0
  5. package/build/cjs/components/Interpretations/DashboardItemInterpretations/index.js +12 -0
  6. package/build/cjs/components/Interpretations/InterpretationModal/Comment.js +12 -17
  7. package/build/cjs/components/Interpretations/InterpretationModal/CommentAddForm.js +20 -34
  8. package/build/cjs/components/Interpretations/InterpretationModal/CommentDeleteButton.js +11 -36
  9. package/build/cjs/components/Interpretations/InterpretationModal/CommentUpdateForm.js +11 -27
  10. package/build/cjs/components/Interpretations/InterpretationModal/InterpretationModal.js +11 -68
  11. package/build/cjs/components/Interpretations/InterpretationModal/InterpretationThread.js +11 -24
  12. package/build/cjs/components/Interpretations/InterpretationsProvider/InterpretationsManager.js +275 -0
  13. package/build/cjs/components/Interpretations/InterpretationsProvider/InterpretationsProvider.js +28 -0
  14. package/build/cjs/components/Interpretations/InterpretationsProvider/__tests__/groupInterpretationIdsByDate.spec.js +37 -0
  15. package/build/cjs/components/Interpretations/InterpretationsProvider/__tests__/hooks.spec.js +565 -0
  16. package/build/cjs/components/Interpretations/InterpretationsProvider/groupInterpretationIdsByDate.js +16 -0
  17. package/build/cjs/components/Interpretations/InterpretationsProvider/hooks.js +278 -0
  18. package/build/cjs/components/Interpretations/InterpretationsProvider/index.js +12 -0
  19. package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationForm.js +25 -30
  20. package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationList.js +8 -38
  21. package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +22 -73
  22. package/build/cjs/components/Interpretations/common/Interpretation/Interpretation.js +20 -34
  23. package/build/cjs/components/Interpretations/common/Interpretation/InterpretationDeleteButton.js +10 -12
  24. package/build/cjs/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js +13 -24
  25. package/build/cjs/components/Interpretations/common/Message/MessageEditorContainer.js +3 -3
  26. package/build/cjs/index.js +72 -63
  27. package/build/cjs/locales/en/translations.json +10 -1
  28. package/build/cjs/modules/pivotTable/getHeaderForDisplay.js +1 -1
  29. package/build/es/__demo__/InterpretationsUnit.stories.js +9 -6
  30. package/build/es/__fixtures__/interpretationsMockData.js +198 -0
  31. package/build/es/components/Interpretations/DashboardItemInterpretations/DashboardInterpretationThread.js +48 -0
  32. package/build/es/components/Interpretations/DashboardItemInterpretations/DashboardItemInterpretations.js +45 -0
  33. package/build/es/components/Interpretations/DashboardItemInterpretations/index.js +1 -0
  34. package/build/es/components/Interpretations/InterpretationModal/Comment.js +14 -19
  35. package/build/es/components/Interpretations/InterpretationModal/CommentAddForm.js +21 -35
  36. package/build/es/components/Interpretations/InterpretationModal/CommentDeleteButton.js +11 -35
  37. package/build/es/components/Interpretations/InterpretationModal/CommentUpdateForm.js +12 -28
  38. package/build/es/components/Interpretations/InterpretationModal/InterpretationModal.js +12 -69
  39. package/build/es/components/Interpretations/InterpretationModal/InterpretationThread.js +11 -24
  40. package/build/es/components/Interpretations/InterpretationsProvider/InterpretationsManager.js +268 -0
  41. package/build/es/components/Interpretations/InterpretationsProvider/InterpretationsProvider.js +19 -0
  42. package/build/es/components/Interpretations/InterpretationsProvider/__tests__/groupInterpretationIdsByDate.spec.js +35 -0
  43. package/build/es/components/Interpretations/InterpretationsProvider/__tests__/hooks.spec.js +561 -0
  44. package/build/es/components/Interpretations/InterpretationsProvider/groupInterpretationIdsByDate.js +9 -0
  45. package/build/es/components/Interpretations/InterpretationsProvider/hooks.js +258 -0
  46. package/build/es/components/Interpretations/InterpretationsProvider/index.js +1 -0
  47. package/build/es/components/Interpretations/InterpretationsUnit/InterpretationForm.js +26 -31
  48. package/build/es/components/Interpretations/InterpretationsUnit/InterpretationList.js +8 -38
  49. package/build/es/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +23 -75
  50. package/build/es/components/Interpretations/common/Interpretation/Interpretation.js +21 -35
  51. package/build/es/components/Interpretations/common/Interpretation/InterpretationDeleteButton.js +11 -13
  52. package/build/es/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js +14 -25
  53. package/build/es/components/Interpretations/common/Message/MessageEditorContainer.js +3 -3
  54. package/build/es/index.js +3 -1
  55. package/build/es/locales/en/translations.json +10 -1
  56. package/build/es/modules/pivotTable/getHeaderForDisplay.js +1 -1
  57. package/package.json +1 -1
  58. package/build/cjs/components/Interpretations/common/Interpretation/useLike.js +0 -56
  59. package/build/es/components/Interpretations/common/Interpretation/useLike.js +0 -50
@@ -0,0 +1,561 @@
1
+ import { renderHook, act, waitFor } from '@testing-library/react';
2
+ import PropTypes from 'prop-types';
3
+ import React from 'react';
4
+ import * as mockData from '../../../../__fixtures__/interpretationsMockData.js';
5
+ import { useInterpretationsList, useActiveInterpretation, useInterpretation, useLike, useInterpretationsCurrentUser, useCreateInterpretation, useDeleteInterpretation, useUpdateInterpretationText, useAddCommentToActiveInterpretation, useDeleteCommentFromActiveInterpretation, useUpdateCommentForActiveInterpretation, useInterpretationAccess, useCommentAccess, useInterpretationsManager } from '../hooks.js';
6
+ import { InterpretationsProvider } from '../InterpretationsProvider.js';
7
+ const mockQuery = jest.fn();
8
+ const mockMutate = jest.fn();
9
+
10
+ // Mock the useDataEngine hook to return our mock
11
+ jest.mock('@dhis2/app-runtime', () => ({
12
+ useDataEngine: () => ({
13
+ ...jest.requireActual('@dhis2/app-runtime'),
14
+ query: mockQuery,
15
+ mutate: mockMutate
16
+ })
17
+ }));
18
+ const createMockImplementation = (data, shouldError = false) => () => new Promise((resolve, reject) => {
19
+ if (shouldError) {
20
+ const msg = typeof data === 'string' ? data : 'OOPSIE';
21
+ reject(new Error(msg));
22
+ } else {
23
+ resolve(data);
24
+ }
25
+ });
26
+
27
+ // Test wrapper component that properly pre-populates manager
28
+ const createWrapper = (initialInterpretations = []) => {
29
+ const InterpretationsPopulator = ({
30
+ children
31
+ }) => {
32
+ const manager = useInterpretationsManager();
33
+ if (initialInterpretations.length > 0 && manager.interpretations.size === 0) {
34
+ manager.currentType = mockData.visualization.type;
35
+ manager.currentVisualizationId = mockData.visualization.id;
36
+ manager.resetInterpretations(initialInterpretations);
37
+ manager.activeInterpretationId = initialInterpretations[0].id;
38
+ }
39
+ return children;
40
+ };
41
+ const TestWrapper = ({
42
+ children
43
+ }) => {
44
+ return /*#__PURE__*/React.createElement(InterpretationsProvider, {
45
+ currentUser: mockData.currentUser
46
+ }, /*#__PURE__*/React.createElement(InterpretationsPopulator, null, children));
47
+ };
48
+ const propTypes = {
49
+ children: PropTypes.node
50
+ };
51
+ TestWrapper.propTypes = propTypes;
52
+ InterpretationsPopulator.propTypes = propTypes;
53
+ return TestWrapper;
54
+ };
55
+
56
+ // Custom hook to test interpretation-focused hooks (with pre-populated manager)
57
+ const useAllHooks = (visualizationType, visualizationId, interpretationId) => {
58
+ // Not used directly but useful here to test things like unsubscriptions etc
59
+ const manager = useInterpretationsManager();
60
+ // Core data hooks - no manual population needed since manager is pre-populated
61
+ const currentUser = useInterpretationsCurrentUser();
62
+ const interpretationsList = useInterpretationsList(visualizationType, visualizationId);
63
+ const activeInterpretation = useActiveInterpretation(interpretationId);
64
+ const interpretation = useInterpretation(interpretationId);
65
+
66
+ // Like hook (includes mutation)
67
+ const like = useLike(interpretationId);
68
+
69
+ // Access hooks
70
+ const interpretationAccess = useInterpretationAccess(interpretation);
71
+ const commentAccess = useCommentAccess(mockData.interpretationDetails.comments[0], true); // Mock comment access
72
+
73
+ // Mutation hooks
74
+ const [createInterpretation, createState] = useCreateInterpretation({
75
+ type: visualizationType,
76
+ id: visualizationId,
77
+ text: 'New interpretation'
78
+ });
79
+
80
+ /* Note that we are NOT deleting the active interpretation but another one!
81
+ * Some of the hooks used in the modal in in this test hook require the
82
+ * presence of an active interpretation and will crash without it. This is
83
+ * not a problem when the components are actually used int an app, because
84
+ * the modal closes when the active interpretation is removed. */
85
+ const [deleteInterpretation, deleteState] = useDeleteInterpretation({
86
+ id: mockData.interpretations[1].id
87
+ });
88
+ const [updateInterpretationText, updateState] = useUpdateInterpretationText({
89
+ id: interpretationId,
90
+ text: 'Updated text'
91
+ });
92
+ const [addComment, addCommentState] = useAddCommentToActiveInterpretation({
93
+ text: 'New comment'
94
+ });
95
+ const [deleteComment, deleteCommentState] = useDeleteCommentFromActiveInterpretation({
96
+ id: 'commentid1'
97
+ });
98
+ const [updateComment, updateCommentState] = useUpdateCommentForActiveInterpretation({
99
+ id: 'commentid1',
100
+ text: 'Updated comment'
101
+ });
102
+ return {
103
+ manager,
104
+ // Data hooks
105
+ currentUser,
106
+ interpretationsList,
107
+ activeInterpretation,
108
+ interpretation,
109
+ // Access hooks
110
+ interpretationAccess,
111
+ commentAccess,
112
+ // Like is also a mutation but does not implement the useInterpretationsManagerMutation internally
113
+ like,
114
+ // Mutation hooks
115
+ mutations: {
116
+ createInterpretation,
117
+ createState,
118
+ deleteInterpretation,
119
+ deleteState,
120
+ updateInterpretationText,
121
+ updateState,
122
+ addComment,
123
+ addCommentState,
124
+ deleteComment,
125
+ deleteCommentState,
126
+ updateComment,
127
+ updateCommentState
128
+ }
129
+ };
130
+ };
131
+
132
+ /* Extracted to as function because it is reused by all other tests
133
+ * apart from the list test */
134
+ const shouldLoadActiveInterpretation = async () => {
135
+ mockQuery.mockImplementationOnce(createMockImplementation({
136
+ interpretations: {
137
+ interpretations: mockData.interpretations
138
+ }
139
+ }));
140
+ mockQuery.mockImplementationOnce(createMockImplementation({
141
+ interpretation: mockData.interpretationDetails
142
+ }));
143
+ const renderHookResult = renderHook(() => useAllHooks(mockData.visualization.type, mockData.visualization.id, mockData.interpretationDetails.id), {
144
+ wrapper: createWrapper(mockData.interpretations)
145
+ });
146
+ const {
147
+ result
148
+ } = renderHookResult;
149
+ expect(result.current.activeInterpretation.loading).toBe(true);
150
+ expect(result.current.activeInterpretation.data).toBeUndefined();
151
+ expect(result.current.activeInterpretation.error).toBeUndefined();
152
+ await waitFor(() => {
153
+ expect(result.current.activeInterpretation.loading).toBe(false);
154
+ expect(result.current.activeInterpretation.error).toBeUndefined();
155
+ expect(result.current.activeInterpretation.data).toEqual(mockData.interpretationDetails);
156
+ });
157
+ return renderHookResult;
158
+ };
159
+ describe('Interpretations hooks integration tests', () => {
160
+ beforeEach(() => {
161
+ mockQuery.mockClear();
162
+ mockMutate.mockClear();
163
+ });
164
+ describe('Listing interpretations', () => {
165
+ test('should show loading state then load interpretations list successfully', async () => {
166
+ mockQuery.mockImplementationOnce(createMockImplementation({
167
+ interpretations: {
168
+ interpretations: mockData.interpretations
169
+ }
170
+ }));
171
+ const {
172
+ result
173
+ } = renderHook(() => useInterpretationsList(mockData.visualization.type, mockData.visualization.id), {
174
+ wrapper: createWrapper()
175
+ });
176
+ expect(result.current.loading).toBe(true);
177
+ expect(result.current.data).toBeUndefined();
178
+ expect(result.current.error).toBeUndefined();
179
+ await waitFor(() => {
180
+ expect(result.current.loading).toBe(false);
181
+ expect(result.current.data).toEqual({
182
+ '2025-09-01': ['interpretation4'],
183
+ '2025-09-02': ['interpretation3'],
184
+ '2025-09-03': ['interpretation2'],
185
+ '2025-09-04': ['interpretation1']
186
+ });
187
+ expect(result.current.error).toBeUndefined();
188
+ });
189
+ });
190
+ test('should handle interpretations list loading error', async () => {
191
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
192
+ const errorMsg = 'Failed to load interpretations';
193
+ try {
194
+ mockQuery.mockImplementationOnce(createMockImplementation(errorMsg, true));
195
+ const {
196
+ result
197
+ } = renderHook(() => useInterpretationsList(mockData.visualization.type, mockData.visualization.id), {
198
+ wrapper: createWrapper()
199
+ });
200
+ expect(result.current.loading).toBe(true);
201
+ expect(result.current.data).toBeUndefined();
202
+ expect(result.current.error).toBeUndefined();
203
+ await waitFor(() => {
204
+ expect(result.current.loading).toBe(false);
205
+ expect(result.current.error).toEqual(new Error(errorMsg));
206
+ expect(result.current.data).toBeUndefined();
207
+ });
208
+ } finally {
209
+ consoleErrorSpy.mockRestore();
210
+ }
211
+ });
212
+ test('should reload when type/id changes', async () => {
213
+ // First load with initial params
214
+ mockQuery.mockImplementationOnce(createMockImplementation({
215
+ interpretations: {
216
+ interpretations: mockData.interpretations
217
+ }
218
+ }));
219
+ const {
220
+ result,
221
+ rerender
222
+ } = renderHook(({
223
+ type,
224
+ id
225
+ }) => useInterpretationsList(type, id), {
226
+ wrapper: createWrapper(),
227
+ initialProps: {
228
+ type: mockData.visualization.type,
229
+ id: mockData.visualization.id
230
+ }
231
+ });
232
+ await waitFor(() => {
233
+ expect(result.current.loading).toBe(false);
234
+ expect(result.current.data).toEqual({
235
+ '2025-09-01': ['interpretation4'],
236
+ '2025-09-02': ['interpretation3'],
237
+ '2025-09-03': ['interpretation2'],
238
+ '2025-09-04': ['interpretation1']
239
+ });
240
+ });
241
+
242
+ // Mock second call with empty result
243
+ mockQuery.mockImplementationOnce(createMockImplementation({
244
+ interpretations: {
245
+ interpretations: []
246
+ }
247
+ }));
248
+
249
+ // Change type/id to trigger reload
250
+ rerender({
251
+ type: 'TABLE',
252
+ id: 'newViz456'
253
+ });
254
+
255
+ // Should show loading again
256
+ expect(result.current.loading).toBe(true);
257
+ await waitFor(() => {
258
+ expect(result.current.loading).toBe(false);
259
+ expect(result.current.data).toEqual({});
260
+ });
261
+
262
+ // Verify that mockQuery was called twice
263
+ expect(mockQuery).toHaveBeenCalledTimes(2);
264
+ });
265
+ });
266
+ describe('Loading active interpretation', () => {
267
+ test('should load active interpretation', async () => {
268
+ await shouldLoadActiveInterpretation();
269
+ });
270
+ test('should handle active interpretation loading error', async () => {
271
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
272
+ const errorMsg = 'Failed to load interpretation details';
273
+ try {
274
+ mockQuery.mockImplementationOnce(createMockImplementation({
275
+ interpretations: {
276
+ interpretations: mockData.interpretations
277
+ }
278
+ }));
279
+ mockQuery.mockImplementationOnce(createMockImplementation(errorMsg, true));
280
+ const {
281
+ result
282
+ } = renderHook(() => useAllHooks(mockData.visualization.type, mockData.visualization.id, mockData.interpretationDetails.id), {
283
+ wrapper: createWrapper(mockData.interpretations)
284
+ });
285
+ expect(result.current.activeInterpretation.loading).toBe(true);
286
+ expect(result.current.activeInterpretation.data).toBeUndefined();
287
+ expect(result.current.activeInterpretation.error).toBeUndefined();
288
+ await waitFor(() => {
289
+ expect(result.current.activeInterpretation.loading).toBe(false);
290
+ expect(result.current.activeInterpretation.error).toEqual(new Error(errorMsg));
291
+ expect(result.current.activeInterpretation.data).toBeUndefined();
292
+ });
293
+ } finally {
294
+ consoleErrorSpy.mockRestore();
295
+ }
296
+ });
297
+ test('should reload when interpretation ID changes', async () => {
298
+ // Mock first interpretation load
299
+ mockQuery.mockImplementationOnce(createMockImplementation({
300
+ interpretations: {
301
+ interpretations: mockData.interpretations
302
+ }
303
+ }));
304
+ mockQuery.mockImplementationOnce(createMockImplementation({
305
+ interpretation: mockData.interpretationDetails
306
+ }));
307
+ const {
308
+ result,
309
+ rerender
310
+ } = renderHook(({
311
+ interpretationId
312
+ }) => useAllHooks(mockData.visualization.type, mockData.visualization.id, interpretationId), {
313
+ wrapper: createWrapper(mockData.interpretations),
314
+ initialProps: {
315
+ interpretationId: mockData.interpretationDetails.id
316
+ }
317
+ });
318
+ await waitFor(() => {
319
+ expect(result.current.activeInterpretation.loading).toBe(false);
320
+ expect(result.current.activeInterpretation.data).toEqual(mockData.interpretationDetails);
321
+ });
322
+
323
+ // Mock second interpretation load
324
+ const secondInterpretation = mockData.interpretations[1];
325
+ mockQuery.mockImplementationOnce(createMockImplementation({
326
+ interpretation: secondInterpretation
327
+ }));
328
+
329
+ // Change interpretation ID to trigger reload
330
+ rerender({
331
+ interpretationId: secondInterpretation.id
332
+ });
333
+
334
+ // Should show loading again
335
+ expect(result.current.activeInterpretation.loading).toBe(true);
336
+ await waitFor(() => {
337
+ expect(result.current.activeInterpretation.loading).toBe(false);
338
+ expect(result.current.activeInterpretation.data).toEqual(secondInterpretation);
339
+ });
340
+
341
+ // Verify that mockQuery was called for both interpretations
342
+ expect(mockQuery).toHaveBeenCalledTimes(3); // 1 for list + 2 for interpretations
343
+ });
344
+ });
345
+ describe('Interpretation subscription', () => {
346
+ // Update behaviour is tested in the mutation hooks
347
+ it('observers subscribe when mounting and unsubscribes when unmounting', async () => {
348
+ const {
349
+ result,
350
+ unmount
351
+ } = await shouldLoadActiveInterpretation();
352
+ const manager = result.current.manager;
353
+ const initialObserversCount = manager.interpretationObservers.get(mockData.interpretationDetails.id).size;
354
+
355
+ // useInterpretation (1), useActiveInterpretation(2)
356
+ expect(initialObserversCount).toBe(2);
357
+ expect(typeof manager.interpretationsListCallback).toBe('function');
358
+ unmount();
359
+ const newObserversCount = manager.interpretationObservers.get(mockData.interpretationDetails.id).size;
360
+ expect(newObserversCount).toBe(0);
361
+ expect(manager.interpretationsListCallback).toBe(null);
362
+ });
363
+ });
364
+ describe('Access and current user hooks', () => {
365
+ test('useInterpretationAccess returns a valid access result', async () => {
366
+ const {
367
+ result
368
+ } = await shouldLoadActiveInterpretation();
369
+ expect(result.current.interpretationAccess).toEqual({
370
+ comment: true,
371
+ delete: true,
372
+ edit: true,
373
+ share: true
374
+ });
375
+ });
376
+ test('useCommentAccess returns a valid access result', async () => {
377
+ const {
378
+ result
379
+ } = await shouldLoadActiveInterpretation();
380
+ expect(result.current.commentAccess).toEqual({
381
+ delete: true,
382
+ edit: true
383
+ });
384
+ });
385
+ test('useInterpretationsCurrentUser returns the current user', async () => {
386
+ const {
387
+ result
388
+ } = await shouldLoadActiveInterpretation();
389
+ expect(result.current.currentUser).toEqual(mockData.currentUser);
390
+ });
391
+ });
392
+ describe('Generic mutation behaviour', () => {
393
+ /* The following hooks all simply call `useInterpretationsManagerMutation`
394
+ * with a different `methodname`:
395
+ * - useCreateInterpretation
396
+ * - useUpdateInterpretationText
397
+ * - useDeleteInterpretation
398
+ * - useAddCommentToActiveInterpretation
399
+ * - useUpdateCommentForActiveInterpretation
400
+ * - useDeleteCommentFromActiveInterpretation
401
+ * So we only have to test one to test the shared behaviour */
402
+ it('can complete successfully after loading', async () => {
403
+ const {
404
+ result
405
+ } = await shouldLoadActiveInterpretation();
406
+ mockMutate.mockImplementationOnce(createMockImplementation({}));
407
+ expect(result.current.mutations.deleteState.loading).toBe(false);
408
+ expect(result.current.mutations.deleteState.error).toBe(undefined);
409
+ expect(result.current.mutations.deleteState.data).toBe(undefined);
410
+ act(() => {
411
+ result.current.mutations.deleteInterpretation();
412
+ });
413
+ await waitFor(() => {
414
+ expect(result.current.mutations.deleteState.loading).toBe(true);
415
+ expect(result.current.mutations.deleteState.error).toBe(undefined);
416
+ expect(result.current.mutations.deleteState.data).toBe(undefined);
417
+ });
418
+ await waitFor(() => {
419
+ expect(result.current.mutations.deleteState.loading).toBe(false);
420
+ expect(result.current.mutations.deleteState.error).toBe(undefined);
421
+ expect(result.current.mutations.deleteState.data).toBe(null);
422
+ });
423
+ });
424
+ it('can return an error after loading', async () => {
425
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
426
+ const errorMsg = 'Failed to delete interpretation';
427
+ try {
428
+ const {
429
+ result
430
+ } = await shouldLoadActiveInterpretation();
431
+ mockMutate.mockImplementationOnce(createMockImplementation(errorMsg, true));
432
+ expect(result.current.mutations.deleteState.loading).toBe(false);
433
+ expect(result.current.mutations.deleteState.error).toBe(undefined);
434
+ expect(result.current.mutations.deleteState.data).toBe(undefined);
435
+ act(() => {
436
+ result.current.mutations.deleteInterpretation();
437
+ });
438
+ await waitFor(() => {
439
+ expect(result.current.mutations.deleteState.loading).toBe(true);
440
+ expect(result.current.mutations.deleteState.error).toBe(undefined);
441
+ expect(result.current.mutations.deleteState.data).toBe(undefined);
442
+ });
443
+ await waitFor(() => {
444
+ expect(result.current.mutations.deleteState.loading).toBe(false);
445
+ expect(result.current.mutations.deleteState.error).toEqual(new Error(errorMsg));
446
+ expect(result.current.mutations.deleteState.data).toBe(undefined);
447
+ });
448
+ } finally {
449
+ consoleErrorSpy.mockRestore();
450
+ }
451
+ });
452
+ });
453
+ describe('Like behaviour', () => {
454
+ test('Interpretation like can be toggled', async () => {
455
+ const {
456
+ result
457
+ } = await shouldLoadActiveInterpretation();
458
+ expect(result.current.like.isLikedByCurrentUser).toBe(true);
459
+ expect(result.current.like.toggleLikeInProgress).toBe(false);
460
+ act(() => {
461
+ result.current.like.toggleLike();
462
+ });
463
+ await waitFor(() => {
464
+ expect(result.current.like.toggleLikeInProgress).toBe(true);
465
+ expect(result.current.like.isLikedByCurrentUser).toBe(true);
466
+ });
467
+ await waitFor(() => {
468
+ expect(result.current.like.toggleLikeInProgress).toBe(false);
469
+ expect(result.current.like.isLikedByCurrentUser).toBe(false);
470
+ // This is also reflected in the interpretation observers
471
+ expect(result.current.interpretation.likes).toBe(0);
472
+ expect(result.current.interpretation.likedBy).toEqual([]);
473
+ expect(result.current.activeInterpretation.data.likes).toBe(0);
474
+ expect(result.current.activeInterpretation.data.likedBy).toEqual([]);
475
+ });
476
+ });
477
+ });
478
+ describe('State synchronization', () => {
479
+ const computeListLength = idsByDate => Object.values(idsByDate).reduce((total, idList) => total + idList.length, 0);
480
+ test('Creating an interpretation updates the list', async () => {
481
+ const {
482
+ result
483
+ } = await shouldLoadActiveInterpretation();
484
+ mockMutate.mockImplementationOnce(createMockImplementation({}));
485
+ mockQuery.mockImplementationOnce(createMockImplementation({
486
+ interpretations: {
487
+ interpretations: mockData.interpretations.concat(mockData.newInterpretation)
488
+ }
489
+ }));
490
+ const initialListLength = computeListLength(result.current.interpretationsList.data);
491
+ act(() => {
492
+ result.current.mutations.createInterpretation();
493
+ });
494
+ await waitFor(() => {
495
+ expect(computeListLength(result.current.interpretationsList.data)).toEqual(initialListLength + 1);
496
+ });
497
+ });
498
+ test('Deleting an interpretation updates the list', async () => {
499
+ const {
500
+ result
501
+ } = await shouldLoadActiveInterpretation();
502
+ mockMutate.mockImplementationOnce(createMockImplementation({}));
503
+ const initialListLength = computeListLength(result.current.interpretationsList.data);
504
+ act(() => {
505
+ result.current.mutations.deleteInterpretation();
506
+ });
507
+ await waitFor(() => {
508
+ expect(computeListLength(result.current.interpretationsList.data)).toBe(initialListLength - 1);
509
+ });
510
+ });
511
+ test('Updating an interpretation text updates the (active) interpretation', async () => {
512
+ const {
513
+ result
514
+ } = await shouldLoadActiveInterpretation();
515
+ mockMutate.mockImplementationOnce(createMockImplementation({}));
516
+ act(() => {
517
+ result.current.mutations.updateInterpretationText();
518
+ });
519
+ await waitFor(() => {
520
+ expect(result.current.interpretation.text).toBe('Updated text');
521
+ expect(result.current.activeInterpretation.data.text).toBe('Updated text');
522
+ });
523
+ });
524
+ /* Note that adding/removing likes also updates the interpretation state, but this
525
+ * behaviour is asserted in the "Like behaviour" tests above */
526
+ test('Adding a comment to an interpretation updates the (active) interpretation', async () => {
527
+ const {
528
+ result
529
+ } = await shouldLoadActiveInterpretation();
530
+ mockMutate.mockImplementationOnce(createMockImplementation({}));
531
+ mockQuery.mockImplementationOnce(createMockImplementation({
532
+ interpretation: {
533
+ ...mockData.interpretationDetails,
534
+ comments: [...mockData.interpretationDetails.comments, mockData.newComment]
535
+ }
536
+ }));
537
+ const initialCommentsLength = result.current.interpretation.comments.length;
538
+ act(() => {
539
+ result.current.mutations.addComment();
540
+ });
541
+ await waitFor(() => {
542
+ expect(result.current.interpretation.comments.length).toBe(initialCommentsLength + 1);
543
+ expect(result.current.activeInterpretation.data.comments.length).toBe(initialCommentsLength + 1);
544
+ });
545
+ });
546
+ test('Removing a comment from an interpretation updates the (active) interpretation', async () => {
547
+ const {
548
+ result
549
+ } = await shouldLoadActiveInterpretation();
550
+ mockMutate.mockImplementationOnce(createMockImplementation({}));
551
+ const initialCommentsLength = result.current.interpretation.comments.length;
552
+ act(() => {
553
+ result.current.mutations.deleteComment();
554
+ });
555
+ await waitFor(() => {
556
+ expect(result.current.interpretation.comments.length).toBe(initialCommentsLength - 1);
557
+ expect(result.current.activeInterpretation.data.comments.length).toBe(initialCommentsLength - 1);
558
+ });
559
+ });
560
+ });
561
+ });
@@ -0,0 +1,9 @@
1
+ export const groupInterpretationIdsByDate = interpretations => interpretations.sort((a, b) => new Date(b.created) - new Date(a.created)).reduce((dateGroups, interpretation) => {
2
+ const dateKey = interpretation.created.split('T')[0];
3
+ if (dateKey in dateGroups) {
4
+ dateGroups[dateKey].push(interpretation.id);
5
+ } else {
6
+ dateGroups[dateKey] = [interpretation.id];
7
+ }
8
+ return dateGroups;
9
+ }, {});