@dhis2/app-service-data 3.2.5 → 3.2.9

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.
@@ -1,76 +1,65 @@
1
- import { render, act, waitFor } from '@testing-library/react';
2
- import React from 'react';
1
+ import { render, waitFor, act } from '@testing-library/react';
2
+ import * as React from 'react';
3
3
  import { CustomDataProvider, DataMutation } from '../react';
4
- const mockBackend = {
5
- target: jest.fn((type, query) => {
6
- expect(query.resource).toBe('target');
7
- expect(type).toBe('create');
8
- expect(query.data).toMatchObject({
9
- question: '?'
10
- });
11
- return Promise.resolve({
12
- answer: 42
13
- });
14
- })
15
- };
16
- describe('Test mutations', () => {
17
- it('Should call the mock callback', async () => {
18
- let doMutation;
19
- const renderFunction = jest.fn(([mutate, {
20
- called,
21
- loading,
22
- error,
23
- data
24
- }]) => {
25
- doMutation = mutate;
26
- if (!called) return 'uncalled';
27
- if (loading) return 'loading';
28
- if (error) return /*#__PURE__*/React.createElement("div", null, "error: ", error.message);
29
- if (data) return /*#__PURE__*/React.createElement("div", null, "data: ", data.answer);
30
- });
31
- const testMutation = {
32
- resource: 'target',
4
+ describe('<DataMutation />', () => {
5
+ it('should render without failing', async () => {
6
+ const endpointSpy = jest.fn(() => Promise.resolve(42));
7
+ const mutation = {
8
+ resource: 'answer',
33
9
  type: 'create',
34
10
  data: {
35
11
  question: '?'
36
12
  }
37
13
  };
38
- const {
39
- getByText
40
- } = render( /*#__PURE__*/React.createElement(CustomDataProvider, {
41
- data: mockBackend
42
- }, /*#__PURE__*/React.createElement(DataMutation, {
43
- mutation: testMutation
44
- }, renderFunction)));
45
- expect(getByText(/uncalled/i)).not.toBeUndefined();
46
- expect(renderFunction).toHaveBeenCalledTimes(1);
47
- expect(mockBackend.target).not.toHaveBeenCalled();
48
- expect(renderFunction).toHaveBeenLastCalledWith([expect.any(Function), {
14
+ const data = {
15
+ answer: endpointSpy
16
+ };
17
+
18
+ const wrapper = ({
19
+ children
20
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
21
+ data: data
22
+ }, children);
23
+
24
+ const renderSpy = jest.fn(() => null);
25
+ render( /*#__PURE__*/React.createElement(DataMutation, {
26
+ mutation: mutation
27
+ }, renderSpy), {
28
+ wrapper
29
+ });
30
+ expect(endpointSpy).toHaveBeenCalledTimes(0);
31
+ expect(renderSpy).toHaveBeenCalledTimes(1);
32
+ expect(renderSpy).toHaveBeenLastCalledWith([expect.any(Function), expect.objectContaining({
49
33
  called: false,
50
34
  loading: false,
51
35
  engine: expect.any(Object)
52
- }]);
53
- expect(doMutation).not.toBeUndefined();
54
- act(() => {
55
- doMutation && doMutation();
36
+ })]);
37
+ await act(async () => {
38
+ const firstRenderSpyCall = renderSpy.mock.calls[0];
39
+ const firstRenderSpyArgument = firstRenderSpyCall[0];
40
+ const [mutate] = firstRenderSpyArgument;
41
+ await mutate();
42
+ });
43
+ waitFor(() => {
44
+ expect(endpointSpy).toHaveBeenCalledTimes(1);
45
+ expect(renderSpy).toHaveBeenCalledTimes(2);
46
+ expect(renderSpy).toHaveBeenLastCalledWith([expect.any(Function), expect.objectContaining({
47
+ called: true,
48
+ loading: true,
49
+ engine: expect.any(Object)
50
+ })]);
51
+ });
52
+ waitFor(() => {
53
+ expect(endpointSpy).toHaveBeenCalledTimes(1);
54
+ expect(renderSpy).toHaveBeenCalledTimes(3);
55
+ expect(renderSpy).toHaveBeenLastCalledWith([expect.any(Function), expect.objectContaining({
56
+ called: true,
57
+ loading: false,
58
+ data: {
59
+ answer: 42
60
+ },
61
+ engine: expect.any(Object)
62
+ })]);
56
63
  });
57
- expect(renderFunction).toHaveBeenCalledTimes(2);
58
- expect(renderFunction).toHaveBeenLastCalledWith([doMutation, {
59
- called: true,
60
- loading: true,
61
- engine: expect.any(Object)
62
- }]);
63
- expect(mockBackend.target).toHaveBeenCalledTimes(1);
64
- await waitFor(() => getByText(/data: /i));
65
- expect(renderFunction).toHaveBeenCalledTimes(3);
66
- expect(renderFunction).toHaveBeenLastCalledWith([doMutation, {
67
- called: true,
68
- loading: false,
69
- data: {
70
- answer: 42
71
- },
72
- engine: expect.any(Object)
73
- }]);
74
- expect(getByText(/data: /i)).toHaveTextContent("data: 42");
75
64
  });
76
65
  });
@@ -23,7 +23,7 @@ export class CustomDataLink {
23
23
 
24
24
  const customResource = this.data[query.resource];
25
25
 
26
- if (!customResource) {
26
+ if (customResource === undefined) {
27
27
  if (this.failOnMiss) {
28
28
  throw new Error("No data provided for resource type ".concat(query.resource, "!"));
29
29
  }
@@ -18,25 +18,32 @@ export const isReplyToMessageConversation = (type, {
18
18
 
19
19
  export const isCreateFeedbackMessage = (type, {
20
20
  resource
21
- }) => type === 'create' && resource === 'messageConversations/feedback'; // POST or PUT to `interpretations/${objectType}/${id}` (add or update an interpretation)
21
+ }) => type === 'create' && resource === 'messageConversations/feedback'; // POST `interpretations/${objectType}/${id}` (add an interpretation to a visualization)
22
22
 
23
- export const isCreateOrUpdateInterpretation = (type, {
23
+ export const isCreateInterpretation = (type, {
24
+ resource
25
+ }) => {
26
+ const pattern = /^interpretations\/(?:reportTable|chart|visualization|map|eventVisualization|eventReport|eventChart|dataSetReport)\/[a-zA-Z0-9]{11}$/;
27
+ return type === 'create' && pattern.test(resource);
28
+ }; // PUT to `interpretations/${id}` (update an interpretation)
29
+
30
+ export const isUpdateInterpretation = (type, {
24
31
  resource,
25
32
  id
26
33
  }) => {
27
- if (type !== 'create' && type !== 'replace') {
34
+ if (type !== 'replace') {
28
35
  return false;
29
36
  }
30
37
 
31
38
  let resourcePattern;
32
39
 
33
- if (type === 'replace' && id) {
34
- resourcePattern = /^interpretations\/(?:reportTable|chart|visualization|map|eventReport|eventChart|dataSetReport)$/;
40
+ if (id) {
41
+ resourcePattern = /^interpretations$/;
35
42
  const idPattern = /^[a-zA-Z0-9]{11}$/;
36
43
  return resourcePattern.test(resource) && idPattern.test(id);
37
44
  }
38
45
 
39
- resourcePattern = /^interpretations\/(?:reportTable|chart|visualization|map|eventReport|eventChart|dataSetReport)\/[a-zA-Z0-9]{11}$/;
46
+ resourcePattern = /^interpretations\/[a-zA-Z0-9]{11}$/;
40
47
  return resourcePattern.test(resource);
41
48
  }; // POST to `interpretations/${id}/comments` (comment on an interpretation)
42
49
 
@@ -1,4 +1,4 @@
1
- import { isReplyToMessageConversation, isCreateFeedbackMessage, isCreateOrUpdateInterpretation, isCommentOnInterpretation, isInterpretationCommentUpdate, isAddOrUpdateSystemOrUserSetting, addOrUpdateConfigurationProperty, isMetadataPackageInstallation } from './textPlainMatchers';
1
+ import { isReplyToMessageConversation, isCreateFeedbackMessage, isCreateInterpretation, isUpdateInterpretation, isCommentOnInterpretation, isInterpretationCommentUpdate, isAddOrUpdateSystemOrUserSetting, addOrUpdateConfigurationProperty, isMetadataPackageInstallation } from './textPlainMatchers';
2
2
  describe('isReplyToMessageConversation', () => {
3
3
  it('retuns true for POST to `messageConversations/${id}`', () => {
4
4
  expect(isReplyToMessageConversation('create', {
@@ -23,30 +23,58 @@ describe('isCreateFeedbackMessage', () => {
23
23
  })).toEqual(false);
24
24
  });
25
25
  });
26
- describe('isCreateOrUpdateInterpretation', () => {
26
+ describe('isCreateInterpretation', () => {
27
27
  it('returns true for a POST to "interpretations/chart/${id}"', () => {
28
- expect(isCreateOrUpdateInterpretation('create', {
28
+ expect(isCreateInterpretation('create', {
29
29
  resource: 'interpretations/chart/oXD88WWSQpR'
30
30
  })).toEqual(true);
31
31
  });
32
- it('returns true for a PUT to "interpretations/chart/${id}"', () => {
33
- expect(isCreateOrUpdateInterpretation('replace', {
32
+ it('returns false for a PUT to "interpretations/chart/${id}"', () => {
33
+ expect(isCreateInterpretation('replace', {
34
34
  resource: 'interpretations/chart/oXD88WWSQpR'
35
+ })).toEqual(false);
36
+ });
37
+ it('retuns false for PATCH requests with a valid query', () => {
38
+ expect(isCreateInterpretation('update', {
39
+ resource: 'interpretations/chart/oXD88WWSQpR'
40
+ })).toEqual(false);
41
+ });
42
+ it('returns false for a request to a different resource', () => {
43
+ expect(isCreateInterpretation('create', {
44
+ resource: 'interpretations/dummy/oXD88WWSQpR'
45
+ })).toEqual(false);
46
+ });
47
+ });
48
+ describe('isUpdateInterpretation', () => {
49
+ it('returns true for a PUT to "interpretations/${id}"', () => {
50
+ expect(isUpdateInterpretation('replace', {
51
+ resource: 'interpretations/oXD88WWSQpR'
35
52
  })).toEqual(true);
36
53
  });
37
54
  it('returns true for PUT with populated query.id', () => {
38
- expect(isCreateOrUpdateInterpretation('replace', {
39
- resource: 'interpretations/chart',
55
+ expect(isUpdateInterpretation('replace', {
56
+ resource: 'interpretations',
40
57
  id: 'oXD88WWSQpR'
41
58
  })).toEqual(true);
42
59
  });
43
- it('retuns false for PATCH requests with a valid query', () => {
44
- expect(isCreateOrUpdateInterpretation('update', {
45
- resource: 'interpretations/chart/oXD88WWSQpR'
60
+ it('returns false for a POST to "interpretations/${id}"', () => {
61
+ expect(isUpdateInterpretation('create', {
62
+ resource: 'interpretations/oXD88WWSQpR'
63
+ })).toEqual(false);
64
+ });
65
+ it('returns false for a PATCH to "interpretations/${id}"', () => {
66
+ expect(isUpdateInterpretation('update', {
67
+ resource: 'interpretations/oXD88WWSQpR'
68
+ })).toEqual(false);
69
+ });
70
+ it('returns false for PATCH with populated query.id', () => {
71
+ expect(isUpdateInterpretation('update', {
72
+ resource: 'interpretations',
73
+ id: 'oXD88WWSQpR'
46
74
  })).toEqual(false);
47
75
  });
48
76
  it('returns false for a request to a different resource', () => {
49
- expect(isCreateOrUpdateInterpretation('create', {
77
+ expect(isUpdateInterpretation('create', {
50
78
  resource: 'interpretations/dummy/oXD88WWSQpR'
51
79
  })).toEqual(false);
52
80
  });
@@ -1,72 +1,337 @@
1
1
  import { renderHook, act } from '@testing-library/react-hooks';
2
- import React from 'react';
2
+ import * as React from 'react';
3
3
  import { CustomDataProvider } from '../components/CustomDataProvider';
4
+ import { useDataEngine } from './useDataEngine';
4
5
  import { useDataMutation } from './useDataMutation';
5
- const customData = {
6
- answer: 42
7
- };
8
-
9
- const wrapper = ({
10
- children
11
- }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
12
- data: customData
13
- }, children);
14
-
15
- const mutation = {
16
- type: 'create',
17
- resource: 'answer',
18
- data: {
19
- answer: 42
20
- }
21
- };
22
- describe('useDataMustation', () => {
23
- const originalError = console.error;
24
- afterEach(() => {
25
- console.error = originalError;
6
+ describe('useDataMutation', () => {
7
+ it('should render without failing', async () => {
8
+ const mutation = {
9
+ type: 'create',
10
+ resource: 'answer',
11
+ data: {
12
+ answer: '?'
13
+ }
14
+ };
15
+ const data = {
16
+ answer: 42
17
+ };
18
+
19
+ const wrapper = ({
20
+ children
21
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
22
+ data: data
23
+ }, children);
24
+
25
+ const {
26
+ result,
27
+ waitFor
28
+ } = renderHook(() => useDataMutation(mutation), {
29
+ wrapper
30
+ });
31
+ const [mutate, beforeMutation] = result.current;
32
+ expect(beforeMutation).toMatchObject({
33
+ loading: false,
34
+ called: false
35
+ });
36
+ act(() => {
37
+ mutate();
38
+ });
39
+ await waitFor(() => {
40
+ const [, duringMutation] = result.current;
41
+ expect(duringMutation).toMatchObject({
42
+ loading: true,
43
+ called: true
44
+ });
45
+ });
46
+ await waitFor(() => {
47
+ const [, afterMutation] = result.current;
48
+ expect(afterMutation).toMatchObject({
49
+ loading: false,
50
+ called: true,
51
+ data: 42
52
+ });
53
+ });
54
+ });
55
+ it('should run immediately with lazy: false', async () => {
56
+ const mutation = {
57
+ type: 'create',
58
+ resource: 'answer',
59
+ data: {
60
+ answer: '?'
61
+ }
62
+ };
63
+ const data = {
64
+ answer: 42
65
+ };
66
+
67
+ const wrapper = ({
68
+ children
69
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
70
+ data: data
71
+ }, children);
72
+
73
+ const {
74
+ result,
75
+ waitFor
76
+ } = renderHook(() => useDataMutation(mutation, {
77
+ lazy: false
78
+ }), {
79
+ wrapper
80
+ });
81
+ const [, duringMutation] = result.current;
82
+ expect(duringMutation).toMatchObject({
83
+ loading: true,
84
+ called: true
85
+ });
86
+ await waitFor(() => {
87
+ const [, afterMutation] = result.current;
88
+ expect(afterMutation).toMatchObject({
89
+ loading: false,
90
+ called: true,
91
+ data: 42
92
+ });
93
+ });
26
94
  });
27
- it('Should render without failing', async () => {
28
- let hookState;
29
- console.error = jest.fn();
95
+ it('should call onComplete on success', async () => {
96
+ const onComplete = jest.fn();
97
+ const mutation = {
98
+ type: 'create',
99
+ resource: 'answer',
100
+ data: {
101
+ answer: '?'
102
+ }
103
+ };
104
+ const data = {
105
+ answer: 42
106
+ };
107
+
108
+ const wrapper = ({
109
+ children
110
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
111
+ data: data
112
+ }, children);
113
+
114
+ const {
115
+ result,
116
+ waitFor
117
+ } = renderHook(() => useDataMutation(mutation, {
118
+ onComplete
119
+ }), {
120
+ wrapper
121
+ });
122
+ expect(onComplete).toHaveBeenCalledTimes(0);
123
+ const [mutate] = result.current;
30
124
  act(() => {
31
- hookState = renderHook(() => useDataMutation(mutation), {
32
- wrapper
125
+ mutate();
126
+ });
127
+ await waitFor(() => {
128
+ const [, state] = result.current;
129
+ expect(state).toMatchObject({
130
+ loading: false,
131
+ called: true,
132
+ data: 42
33
133
  });
134
+ expect(onComplete).toHaveBeenCalledTimes(1);
135
+ expect(onComplete).toHaveBeenLastCalledWith(42);
34
136
  });
35
- let [mutate, state] = hookState.result.current;
36
- expect(state).toMatchObject({
37
- called: false,
38
- loading: false
137
+ });
138
+ it('should call onError on error', async () => {
139
+ const error = new Error('Something went wrong');
140
+ const onError = jest.fn();
141
+ const mutation = {
142
+ type: 'create',
143
+ resource: 'answer',
144
+ data: {
145
+ answer: 42
146
+ }
147
+ };
148
+ const data = {
149
+ answer: () => {
150
+ throw error;
151
+ }
152
+ };
153
+
154
+ const wrapper = ({
155
+ children
156
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
157
+ data: data
158
+ }, children);
159
+
160
+ const {
161
+ result,
162
+ waitFor
163
+ } = renderHook(() => useDataMutation(mutation, {
164
+ onError
165
+ }), {
166
+ wrapper
39
167
  });
168
+ expect(onError).toHaveBeenCalledTimes(0);
169
+ const [mutate] = result.current;
40
170
  act(() => {
41
171
  mutate();
42
172
  });
43
- mutate = hookState.result.current[0];
44
- state = hookState.result.current[1];
45
- expect(state).toMatchObject({
46
- called: true,
47
- loading: true
173
+ await waitFor(() => {
174
+ const [, state] = result.current;
175
+ expect(state).toMatchObject({
176
+ loading: false,
177
+ called: true,
178
+ error
179
+ });
48
180
  });
181
+ expect(onError).toHaveBeenCalledTimes(1);
182
+ expect(onError).toHaveBeenLastCalledWith(error);
49
183
  });
50
- it('Should run immediately with lazy: false', async () => {
51
- let hookState;
52
- console.error = jest.fn();
184
+ it('should resolve variables', async () => {
185
+ const mutation = {
186
+ type: 'update',
187
+ resource: 'answer',
188
+ id: ({
189
+ id
190
+ }) => id,
191
+ data: {
192
+ answer: '?'
193
+ }
194
+ };
195
+ const answerSpy = jest.fn(() => 42);
196
+ const data = {
197
+ answer: answerSpy
198
+ };
199
+
200
+ const wrapper = ({
201
+ children
202
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
203
+ data: data
204
+ }, children);
205
+
206
+ const {
207
+ result,
208
+ waitFor
209
+ } = renderHook(() => useDataMutation(mutation, {
210
+ lazy: false,
211
+ variables: {
212
+ id: '1'
213
+ }
214
+ }), {
215
+ wrapper
216
+ });
217
+ await waitFor(() => {
218
+ expect(answerSpy).toHaveBeenLastCalledWith(expect.any(String), expect.objectContaining({
219
+ id: '1'
220
+ }), expect.any(Object));
221
+ });
222
+ const [mutate] = result.current;
53
223
  act(() => {
54
- hookState = renderHook(() => useDataMutation(mutation, {
55
- lazy: false
56
- }), {
57
- wrapper
224
+ mutate({
225
+ id: '2'
58
226
  });
59
227
  });
60
- let [, state] = hookState.result.current;
61
- expect(state).toMatchObject({
62
- called: true,
63
- loading: true
228
+ await waitFor(() => {
229
+ expect(answerSpy).toHaveBeenLastCalledWith(expect.any(String), expect.objectContaining({
230
+ id: '2'
231
+ }), expect.any(Object));
64
232
  });
65
- await hookState.waitForNextUpdate();
66
- state = hookState.result.current[1];
233
+ });
234
+ it('should return a reference to the engine', async () => {
235
+ const mutation = {
236
+ type: 'create',
237
+ resource: 'answer',
238
+ data: {
239
+ answer: '?'
240
+ }
241
+ };
242
+
243
+ const wrapper = ({
244
+ children
245
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
246
+ data: {}
247
+ }, children);
248
+
249
+ const engineHook = renderHook(() => useDataEngine(), {
250
+ wrapper
251
+ });
252
+ const mutationHook = renderHook(() => useDataMutation(mutation), {
253
+ wrapper
254
+ });
255
+ /**
256
+ * Ideally we'd check referential equality here with .toBe, but since
257
+ * both hooks run in a different context that doesn't work.
258
+ */
259
+
260
+ expect(mutationHook.result.current[1].engine).toStrictEqual(engineHook.result.current);
261
+ });
262
+ it('should return a stable mutate function', async () => {
263
+ const mutation = {
264
+ type: 'create',
265
+ resource: 'answer',
266
+ data: {
267
+ answer: '?'
268
+ }
269
+ };
270
+ const data = {
271
+ answer: 42
272
+ };
273
+
274
+ const wrapper = ({
275
+ children
276
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
277
+ data: data
278
+ }, children);
279
+
280
+ const {
281
+ result
282
+ } = renderHook(() => useDataMutation(mutation), {
283
+ wrapper
284
+ });
285
+ const [firstMutate] = result.current;
286
+ await act(async () => {
287
+ await firstMutate({
288
+ variable: 'variable'
289
+ });
290
+ });
291
+ const [secondMutate, state] = result.current;
67
292
  expect(state).toMatchObject({
68
293
  loading: false,
69
- data: 42
294
+ called: true
295
+ });
296
+ expect(firstMutate).toBe(secondMutate);
297
+ });
298
+ it('should resolve with the data from mutate on success', async () => {
299
+ const mutation = {
300
+ type: 'create',
301
+ resource: 'answer',
302
+ data: {
303
+ answer: '?'
304
+ }
305
+ };
306
+ const data = {
307
+ answer: 42
308
+ };
309
+
310
+ const wrapper = ({
311
+ children
312
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
313
+ data: data
314
+ }, children);
315
+
316
+ const {
317
+ result,
318
+ waitFor
319
+ } = renderHook(() => useDataMutation(mutation), {
320
+ wrapper
321
+ });
322
+ let mutatePromise;
323
+ const [mutate] = result.current;
324
+ act(() => {
325
+ mutatePromise = mutate();
326
+ });
327
+ await waitFor(() => {
328
+ const [, state] = result.current;
329
+ expect(state).toMatchObject({
330
+ loading: false,
331
+ called: true,
332
+ data: 42
333
+ });
334
+ expect(mutatePromise).resolves.toBe(42);
70
335
  });
71
336
  });
72
337
  });
@@ -104,10 +104,6 @@ export const useDataQuery = (query, {
104
104
  }) => data);
105
105
  }
106
106
 
107
- if (!enabled) {
108
- setEnabled(true);
109
- }
110
-
111
107
  if (newVariables) {
112
108
  // Use cached hash if it exists
113
109
  const currentHash = variablesHash.current || stableVariablesHash(variables);
@@ -117,8 +113,11 @@ export const useDataQuery = (query, {
117
113
  const mergedHash = stableVariablesHash(mergedVariables);
118
114
  const identical = currentHash === mergedHash;
119
115
 
120
- if (identical) {
121
- // If the variables are identical we'll need to trigger the refetch manually
116
+ if (identical && enabled) {
117
+ /**
118
+ * If the variables are identical and the query is enabled
119
+ * we'll need to trigger the refetch manually
120
+ */
122
121
  return queryRefetch({
123
122
  cancelRefetch: true,
124
123
  throwOnError: false
@@ -129,6 +128,11 @@ export const useDataQuery = (query, {
129
128
  variablesHash.current = mergedHash;
130
129
  setVariables(mergedVariables);
131
130
  }
131
+ } // Enable the query after the variables have been set to prevent extra request
132
+
133
+
134
+ if (!enabled) {
135
+ setEnabled(true);
132
136
  } // This promise does not currently reject on errors
133
137
 
134
138