@dhis2/app-service-data 3.2.4 → 3.2.8

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.
@@ -2,79 +2,347 @@
2
2
 
3
3
  var _reactHooks = require("@testing-library/react-hooks");
4
4
 
5
- var _react = _interopRequireDefault(require("react"));
5
+ var React = _interopRequireWildcard(require("react"));
6
6
 
7
7
  var _CustomDataProvider = require("../components/CustomDataProvider");
8
8
 
9
+ var _useDataEngine = require("./useDataEngine");
10
+
9
11
  var _useDataMutation = require("./useDataMutation");
10
12
 
11
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
-
13
- const customData = {
14
- answer: 42
15
- };
16
-
17
- const wrapper = ({
18
- children
19
- }) => /*#__PURE__*/_react.default.createElement(_CustomDataProvider.CustomDataProvider, {
20
- data: customData
21
- }, children);
22
-
23
- const mutation = {
24
- type: 'create',
25
- resource: 'answer',
26
- data: {
27
- answer: 42
28
- }
29
- };
30
- describe('useDataMustation', () => {
31
- const originalError = console.error;
32
- afterEach(() => {
33
- console.error = originalError;
13
+ function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
14
+
15
+ function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
16
+
17
+ describe('useDataMutation', () => {
18
+ it('should render without failing', async () => {
19
+ const mutation = {
20
+ type: 'create',
21
+ resource: 'answer',
22
+ data: {
23
+ answer: '?'
24
+ }
25
+ };
26
+ const data = {
27
+ answer: 42
28
+ };
29
+
30
+ const wrapper = ({
31
+ children
32
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
33
+ data: data
34
+ }, children);
35
+
36
+ const {
37
+ result,
38
+ waitFor
39
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation), {
40
+ wrapper
41
+ });
42
+ const [mutate, beforeMutation] = result.current;
43
+ expect(beforeMutation).toMatchObject({
44
+ loading: false,
45
+ called: false
46
+ });
47
+ (0, _reactHooks.act)(() => {
48
+ mutate();
49
+ });
50
+ await waitFor(() => {
51
+ const [, duringMutation] = result.current;
52
+ expect(duringMutation).toMatchObject({
53
+ loading: true,
54
+ called: true
55
+ });
56
+ });
57
+ await waitFor(() => {
58
+ const [, afterMutation] = result.current;
59
+ expect(afterMutation).toMatchObject({
60
+ loading: false,
61
+ called: true,
62
+ data: 42
63
+ });
64
+ });
34
65
  });
35
- it('Should render without failing', async () => {
36
- let hookState;
37
- console.error = jest.fn();
66
+ it('should run immediately with lazy: false', async () => {
67
+ const mutation = {
68
+ type: 'create',
69
+ resource: 'answer',
70
+ data: {
71
+ answer: '?'
72
+ }
73
+ };
74
+ const data = {
75
+ answer: 42
76
+ };
77
+
78
+ const wrapper = ({
79
+ children
80
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
81
+ data: data
82
+ }, children);
83
+
84
+ const {
85
+ result,
86
+ waitFor
87
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation, {
88
+ lazy: false
89
+ }), {
90
+ wrapper
91
+ });
92
+ const [, duringMutation] = result.current;
93
+ expect(duringMutation).toMatchObject({
94
+ loading: true,
95
+ called: true
96
+ });
97
+ await waitFor(() => {
98
+ const [, afterMutation] = result.current;
99
+ expect(afterMutation).toMatchObject({
100
+ loading: false,
101
+ called: true,
102
+ data: 42
103
+ });
104
+ });
105
+ });
106
+ it('should call onComplete on success', async () => {
107
+ const onComplete = jest.fn();
108
+ const mutation = {
109
+ type: 'create',
110
+ resource: 'answer',
111
+ data: {
112
+ answer: '?'
113
+ }
114
+ };
115
+ const data = {
116
+ answer: 42
117
+ };
118
+
119
+ const wrapper = ({
120
+ children
121
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
122
+ data: data
123
+ }, children);
124
+
125
+ const {
126
+ result,
127
+ waitFor
128
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation, {
129
+ onComplete
130
+ }), {
131
+ wrapper
132
+ });
133
+ expect(onComplete).toHaveBeenCalledTimes(0);
134
+ const [mutate] = result.current;
38
135
  (0, _reactHooks.act)(() => {
39
- hookState = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation), {
40
- wrapper
136
+ mutate();
137
+ });
138
+ await waitFor(() => {
139
+ const [, state] = result.current;
140
+ expect(state).toMatchObject({
141
+ loading: false,
142
+ called: true,
143
+ data: 42
41
144
  });
145
+ expect(onComplete).toHaveBeenCalledTimes(1);
146
+ expect(onComplete).toHaveBeenLastCalledWith(42);
42
147
  });
43
- let [mutate, state] = hookState.result.current;
44
- expect(state).toMatchObject({
45
- called: false,
46
- loading: false
148
+ });
149
+ it('should call onError on error', async () => {
150
+ const error = new Error('Something went wrong');
151
+ const onError = jest.fn();
152
+ const mutation = {
153
+ type: 'create',
154
+ resource: 'answer',
155
+ data: {
156
+ answer: 42
157
+ }
158
+ };
159
+ const data = {
160
+ answer: () => {
161
+ throw error;
162
+ }
163
+ };
164
+
165
+ const wrapper = ({
166
+ children
167
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
168
+ data: data
169
+ }, children);
170
+
171
+ const {
172
+ result,
173
+ waitFor
174
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation, {
175
+ onError
176
+ }), {
177
+ wrapper
47
178
  });
179
+ expect(onError).toHaveBeenCalledTimes(0);
180
+ const [mutate] = result.current;
48
181
  (0, _reactHooks.act)(() => {
49
182
  mutate();
50
183
  });
51
- mutate = hookState.result.current[0];
52
- state = hookState.result.current[1];
53
- expect(state).toMatchObject({
54
- called: true,
55
- loading: true
184
+ await waitFor(() => {
185
+ const [, state] = result.current;
186
+ expect(state).toMatchObject({
187
+ loading: false,
188
+ called: true,
189
+ error
190
+ });
56
191
  });
192
+ expect(onError).toHaveBeenCalledTimes(1);
193
+ expect(onError).toHaveBeenLastCalledWith(error);
57
194
  });
58
- it('Should run immediately with lazy: false', async () => {
59
- let hookState;
60
- console.error = jest.fn();
195
+ it('should resolve variables', async () => {
196
+ const mutation = {
197
+ type: 'update',
198
+ resource: 'answer',
199
+ id: ({
200
+ id
201
+ }) => id,
202
+ data: {
203
+ answer: '?'
204
+ }
205
+ };
206
+ const answerSpy = jest.fn(() => 42);
207
+ const data = {
208
+ answer: answerSpy
209
+ };
210
+
211
+ const wrapper = ({
212
+ children
213
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
214
+ data: data
215
+ }, children);
216
+
217
+ const {
218
+ result,
219
+ waitFor
220
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation, {
221
+ lazy: false,
222
+ variables: {
223
+ id: '1'
224
+ }
225
+ }), {
226
+ wrapper
227
+ });
228
+ await waitFor(() => {
229
+ expect(answerSpy).toHaveBeenLastCalledWith(expect.any(String), expect.objectContaining({
230
+ id: '1'
231
+ }), expect.any(Object));
232
+ });
233
+ const [mutate] = result.current;
61
234
  (0, _reactHooks.act)(() => {
62
- hookState = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation, {
63
- lazy: false
64
- }), {
65
- wrapper
235
+ mutate({
236
+ id: '2'
66
237
  });
67
238
  });
68
- let [, state] = hookState.result.current;
69
- expect(state).toMatchObject({
70
- called: true,
71
- loading: true
239
+ await waitFor(() => {
240
+ expect(answerSpy).toHaveBeenLastCalledWith(expect.any(String), expect.objectContaining({
241
+ id: '2'
242
+ }), expect.any(Object));
72
243
  });
73
- await hookState.waitForNextUpdate();
74
- state = hookState.result.current[1];
244
+ });
245
+ it('should return a reference to the engine', async () => {
246
+ const mutation = {
247
+ type: 'create',
248
+ resource: 'answer',
249
+ data: {
250
+ answer: '?'
251
+ }
252
+ };
253
+
254
+ const wrapper = ({
255
+ children
256
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
257
+ data: {}
258
+ }, children);
259
+
260
+ const engineHook = (0, _reactHooks.renderHook)(() => (0, _useDataEngine.useDataEngine)(), {
261
+ wrapper
262
+ });
263
+ const mutationHook = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation), {
264
+ wrapper
265
+ });
266
+ /**
267
+ * Ideally we'd check referential equality here with .toBe, but since
268
+ * both hooks run in a different context that doesn't work.
269
+ */
270
+
271
+ expect(mutationHook.result.current[1].engine).toStrictEqual(engineHook.result.current);
272
+ });
273
+ it('should return a stable mutate function', async () => {
274
+ const mutation = {
275
+ type: 'create',
276
+ resource: 'answer',
277
+ data: {
278
+ answer: '?'
279
+ }
280
+ };
281
+ const data = {
282
+ answer: 42
283
+ };
284
+
285
+ const wrapper = ({
286
+ children
287
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
288
+ data: data
289
+ }, children);
290
+
291
+ const {
292
+ result
293
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation), {
294
+ wrapper
295
+ });
296
+ const [firstMutate] = result.current;
297
+ await (0, _reactHooks.act)(async () => {
298
+ await firstMutate({
299
+ variable: 'variable'
300
+ });
301
+ });
302
+ const [secondMutate, state] = result.current;
75
303
  expect(state).toMatchObject({
76
304
  loading: false,
77
- data: 42
305
+ called: true
306
+ });
307
+ expect(firstMutate).toBe(secondMutate);
308
+ });
309
+ it('should resolve with the data from mutate on success', async () => {
310
+ const mutation = {
311
+ type: 'create',
312
+ resource: 'answer',
313
+ data: {
314
+ answer: '?'
315
+ }
316
+ };
317
+ const data = {
318
+ answer: 42
319
+ };
320
+
321
+ const wrapper = ({
322
+ children
323
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
324
+ data: data
325
+ }, children);
326
+
327
+ const {
328
+ result,
329
+ waitFor
330
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataMutation.useDataMutation)(mutation), {
331
+ wrapper
332
+ });
333
+ let mutatePromise;
334
+ const [mutate] = result.current;
335
+ (0, _reactHooks.act)(() => {
336
+ mutatePromise = mutate();
337
+ });
338
+ await waitFor(() => {
339
+ const [, state] = result.current;
340
+ expect(state).toMatchObject({
341
+ loading: false,
342
+ called: true,
343
+ data: 42
344
+ });
345
+ expect(mutatePromise).resolves.toBe(42);
78
346
  });
79
347
  });
80
348
  });
@@ -96,9 +96,13 @@ const useDataQuery = (query, {
96
96
  /**
97
97
  * Refetch allows a user to update the variables or just
98
98
  * trigger a refetch of the query with the current variables.
99
+ *
100
+ * We're using useCallback to make the identity of the function
101
+ * as stable as possible, so that it won't trigger excessive
102
+ * rerenders when used for side-effects.
99
103
  */
100
104
 
101
- const refetch = newVariables => {
105
+ const refetch = (0, _react.useCallback)(newVariables => {
102
106
  /**
103
107
  * If there are no updates that will trigger an automatic refetch
104
108
  * we'll need to call react-query's refetch directly
@@ -112,10 +116,6 @@ const useDataQuery = (query, {
112
116
  }) => data);
113
117
  }
114
118
 
115
- if (!enabled) {
116
- setEnabled(true);
117
- }
118
-
119
119
  if (newVariables) {
120
120
  // Use cached hash if it exists
121
121
  const currentHash = variablesHash.current || (0, _stableVariablesHash.stableVariablesHash)(variables);
@@ -125,8 +125,11 @@ const useDataQuery = (query, {
125
125
  const mergedHash = (0, _stableVariablesHash.stableVariablesHash)(mergedVariables);
126
126
  const identical = currentHash === mergedHash;
127
127
 
128
- if (identical) {
129
- // If the variables are identical we'll need to trigger the refetch manually
128
+ if (identical && enabled) {
129
+ /**
130
+ * If the variables are identical and the query is enabled
131
+ * we'll need to trigger the refetch manually
132
+ */
130
133
  return queryRefetch({
131
134
  cancelRefetch: true,
132
135
  throwOnError: false
@@ -137,6 +140,11 @@ const useDataQuery = (query, {
137
140
  variablesHash.current = mergedHash;
138
141
  setVariables(mergedVariables);
139
142
  }
143
+ } // Enable the query after the variables have been set to prevent extra request
144
+
145
+
146
+ if (!enabled) {
147
+ setEnabled(true);
140
148
  } // This promise does not currently reject on errors
141
149
 
142
150
 
@@ -145,13 +153,12 @@ const useDataQuery = (query, {
145
153
  resolve(data);
146
154
  };
147
155
  });
148
- };
156
+ }, [enabled, queryRefetch, variables]);
149
157
  /**
150
158
  * react-query returns null or an error, but we return undefined
151
159
  * or an error, so this ensures consistency with the other types.
152
160
  */
153
161
 
154
-
155
162
  const ourError = error || undefined;
156
163
  return {
157
164
  engine,
@@ -507,6 +507,149 @@ describe('useDataQuery', () => {
507
507
  });
508
508
  });
509
509
  describe('return values: refetch', () => {
510
+ it('Should only trigger a single request when refetch is called on a lazy query with new variables', async () => {
511
+ const spy = jest.fn((type, query) => {
512
+ if (query.id === '1') {
513
+ return 42;
514
+ }
515
+
516
+ return 0;
517
+ });
518
+ const data = {
519
+ answer: spy
520
+ };
521
+ const query = {
522
+ x: {
523
+ resource: 'answer',
524
+ id: ({
525
+ id
526
+ }) => id
527
+ }
528
+ };
529
+
530
+ const wrapper = ({
531
+ children
532
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
533
+ data: data
534
+ }, children);
535
+
536
+ const {
537
+ result,
538
+ waitFor
539
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataQuery.useDataQuery)(query, {
540
+ lazy: true
541
+ }), {
542
+ wrapper
543
+ });
544
+ expect(spy).not.toHaveBeenCalled();
545
+ (0, _reactHooks.act)(() => {
546
+ result.current.refetch({
547
+ id: '1'
548
+ });
549
+ });
550
+ await waitFor(() => {
551
+ expect(result.current).toMatchObject({
552
+ loading: false,
553
+ called: true,
554
+ data: {
555
+ x: 42
556
+ }
557
+ });
558
+ });
559
+ expect(spy).toHaveBeenCalledTimes(1);
560
+ });
561
+ it('Should only trigger a single request when refetch is called on a lazy query with identical variables', async () => {
562
+ const spy = jest.fn((type, query) => {
563
+ if (query.id === '1') {
564
+ return 42;
565
+ }
566
+
567
+ return 0;
568
+ });
569
+ const data = {
570
+ answer: spy
571
+ };
572
+ const query = {
573
+ x: {
574
+ resource: 'answer',
575
+ id: ({
576
+ id
577
+ }) => id
578
+ }
579
+ };
580
+
581
+ const wrapper = ({
582
+ children
583
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
584
+ data: data
585
+ }, children);
586
+
587
+ const {
588
+ result,
589
+ waitFor
590
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataQuery.useDataQuery)(query, {
591
+ lazy: true,
592
+ variables: {
593
+ id: '1'
594
+ }
595
+ }), {
596
+ wrapper
597
+ });
598
+ expect(spy).not.toHaveBeenCalled();
599
+ (0, _reactHooks.act)(() => {
600
+ result.current.refetch({
601
+ id: '1'
602
+ });
603
+ });
604
+ await waitFor(() => {
605
+ expect(result.current).toMatchObject({
606
+ loading: false,
607
+ called: true,
608
+ data: {
609
+ x: 42
610
+ }
611
+ });
612
+ });
613
+ expect(spy).toHaveBeenCalledTimes(1);
614
+ });
615
+ it('Should have a stable identity if the variables have not changed', async () => {
616
+ const data = {
617
+ answer: () => 42
618
+ };
619
+ const query = {
620
+ x: {
621
+ resource: 'answer'
622
+ }
623
+ };
624
+
625
+ const wrapper = ({
626
+ children
627
+ }) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
628
+ data: data
629
+ }, children);
630
+
631
+ const {
632
+ result,
633
+ waitForNextUpdate,
634
+ rerender
635
+ } = (0, _reactHooks.renderHook)(() => (0, _useDataQuery.useDataQuery)(query), {
636
+ wrapper
637
+ });
638
+ const firstRefetch = result.current.refetch;
639
+ await waitForNextUpdate();
640
+ (0, _reactHooks.act)(() => {
641
+ result.current.refetch();
642
+ /**
643
+ * FIXME: https://github.com/tannerlinsley/react-query/issues/2481
644
+ * This forced rerender is not necessary in the app, just when testing.
645
+ * It is unclear why.
646
+ */
647
+
648
+ rerender();
649
+ });
650
+ await waitForNextUpdate();
651
+ expect(result.current.refetch).toBe(firstRefetch);
652
+ });
510
653
  it('Should return stale data and set loading to true on refetch', async () => {
511
654
  const answers = [42, 43];
512
655
  const mockSpy = jest.fn(() => Promise.resolve(answers.shift()));