@dhis2/app-service-data 3.17.0 → 3.17.2

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 (34) hide show
  1. package/.gitignore +5 -0
  2. package/d2.config.js +9 -0
  3. package/jest.config.js +14 -0
  4. package/package.json +4 -4
  5. package/src/__tests__/integration.test.tsx +80 -0
  6. package/src/__tests__/mutations.test.tsx +71 -0
  7. package/src/index.ts +5 -0
  8. package/src/react/components/CustomDataProvider.tsx +33 -0
  9. package/src/react/components/DataMutation.tsx +24 -0
  10. package/src/react/components/DataProvider.test.tsx +22 -0
  11. package/src/react/components/DataProvider.tsx +58 -0
  12. package/src/react/components/DataQuery.tsx +26 -0
  13. package/src/react/components/index.ts +4 -0
  14. package/src/react/context/DataContext.tsx +5 -0
  15. package/src/react/context/defaultDataContext.test.ts +46 -0
  16. package/src/react/context/defaultDataContext.ts +9 -0
  17. package/src/react/hooks/index.ts +3 -0
  18. package/src/react/hooks/mergeAndCompareVariables.test.ts +65 -0
  19. package/src/react/hooks/mergeAndCompareVariables.ts +32 -0
  20. package/src/react/hooks/stableVariablesHash.test.ts +53 -0
  21. package/src/react/hooks/stableVariablesHash.ts +56 -0
  22. package/src/react/hooks/useDataEngine.ts +8 -0
  23. package/src/react/hooks/useDataMutation.test.tsx +296 -0
  24. package/src/react/hooks/useDataMutation.ts +42 -0
  25. package/src/react/hooks/useDataQuery.test.tsx +1029 -0
  26. package/src/react/hooks/useDataQuery.ts +170 -0
  27. package/src/react/hooks/useQueryExecutor.test.tsx +195 -0
  28. package/src/react/hooks/useQueryExecutor.ts +99 -0
  29. package/src/react/hooks/useStaticInput.test.ts +101 -0
  30. package/src/react/hooks/useStaticInput.ts +27 -0
  31. package/src/react/index.ts +2 -0
  32. package/src/setupRTL.ts +5 -0
  33. package/src/types.ts +72 -0
  34. package/tsconfig.json +10 -0
@@ -0,0 +1,170 @@
1
+ import type {
2
+ Query,
3
+ QueryOptions,
4
+ QueryResult,
5
+ QueryVariables,
6
+ FetchError,
7
+ } from '@dhis2/data-engine'
8
+ import { useQuery } from '@tanstack/react-query'
9
+ import { useState, useRef, useCallback, useDebugValue } from 'react'
10
+ import type { QueryRenderInput, QueryRefetchFunction } from '../../types'
11
+ import { mergeAndCompareVariables } from './mergeAndCompareVariables'
12
+ import { useDataEngine } from './useDataEngine'
13
+ import { useStaticInput } from './useStaticInput'
14
+
15
+ type QueryState = {
16
+ enabled: boolean
17
+ variables?: QueryVariables
18
+ variablesHash?: string
19
+ refetchCallback?: (data: any) => void
20
+ }
21
+
22
+ export const useDataQuery = <TQueryResult = QueryResult>(
23
+ query: Query,
24
+ {
25
+ onComplete: userOnSuccess,
26
+ onError: userOnError,
27
+ variables: initialVariables = {},
28
+ lazy: initialLazy = false,
29
+ }: QueryOptions<TQueryResult> = {}
30
+ ): QueryRenderInput<TQueryResult> => {
31
+ const [staticQuery] = useStaticInput<Query>(query, {
32
+ warn: true,
33
+ name: 'query',
34
+ })
35
+ const [variablesUpdateCount, setVariablesUpdateCount] = useState(0)
36
+
37
+ const queryState = useRef<QueryState>({
38
+ variables: initialVariables,
39
+ variablesHash: undefined,
40
+ enabled: !initialLazy,
41
+ refetchCallback: undefined,
42
+ })
43
+
44
+ /**
45
+ * Display current query state and refetch count in React DevTools
46
+ */
47
+
48
+ useDebugValue(
49
+ {
50
+ variablesUpdateCount,
51
+ enabled: queryState.current.enabled,
52
+ variables: queryState.current.variables,
53
+ },
54
+ (debugValue) => JSON.stringify(debugValue)
55
+ )
56
+
57
+ /**
58
+ * User callbacks and refetch handling
59
+ */
60
+
61
+ const onSuccess = (data: any) => {
62
+ queryState.current.refetchCallback?.(data)
63
+ queryState.current.refetchCallback = undefined
64
+
65
+ if (userOnSuccess) {
66
+ userOnSuccess(data)
67
+ }
68
+ }
69
+
70
+ const onError = (error: FetchError) => {
71
+ // If we'd want to reject on errors we'd call the cb with the error here
72
+ queryState.current.refetchCallback = undefined
73
+
74
+ if (userOnError) {
75
+ userOnError(error)
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Setting up react-query
81
+ */
82
+
83
+ const engine = useDataEngine()
84
+ const queryKey = [staticQuery, queryState.current.variables]
85
+ const queryFn = () =>
86
+ engine.query(staticQuery, { variables: queryState.current.variables })
87
+
88
+ const {
89
+ status,
90
+ fetchStatus,
91
+ error,
92
+ data,
93
+ refetch: queryRefetch,
94
+ } = useQuery({
95
+ queryKey,
96
+ queryFn,
97
+ enabled: queryState.current.enabled,
98
+ onSuccess,
99
+ onError,
100
+ })
101
+
102
+ /**
103
+ * Refetch allows a user to update the variables or just
104
+ * trigger a refetch of the query with the current variables.
105
+ *
106
+ * We're using useCallback to make the identity of the function
107
+ * as stable as possible, so that it won't trigger excessive
108
+ * rerenders when used for side-effects.
109
+ */
110
+
111
+ const refetch: QueryRefetchFunction = useCallback(
112
+ (newVariables) => {
113
+ const { identical, mergedVariables, mergedVariablesHash } =
114
+ mergeAndCompareVariables(
115
+ queryState.current.variables,
116
+ newVariables,
117
+ queryState.current.variablesHash
118
+ )
119
+
120
+ /**
121
+ * If there are no updates that will trigger an automatic refetch
122
+ * we'll need to call react-query's refetch directly
123
+ */
124
+ if (queryState.current.enabled && identical) {
125
+ return queryRefetch({ throwOnError: false }).then(
126
+ ({ data }) => data
127
+ )
128
+ }
129
+
130
+ queryState.current.variables = mergedVariables
131
+ queryState.current.variablesHash = mergedVariablesHash
132
+ queryState.current.enabled = true
133
+
134
+ // This promise does not currently reject on errors
135
+ const refetchPromise = new Promise<TQueryResult>((resolve) => {
136
+ queryState.current.refetchCallback = (data) => {
137
+ resolve(data)
138
+ }
139
+ })
140
+
141
+ // Trigger a react-query refetch by incrementing variablesUpdateCount state
142
+ setVariablesUpdateCount((prevCount) => prevCount + 1)
143
+
144
+ return refetchPromise
145
+ },
146
+ [queryRefetch]
147
+ )
148
+
149
+ /**
150
+ * react-query returns null or an error, but we return undefined
151
+ * or an error, so this ensures consistency with the other types.
152
+ */
153
+ const ourError = error || undefined
154
+
155
+ return {
156
+ engine,
157
+ // A query has not been called if it is lazy (fetchStatus = 'idle') and no initial data is available (status = 'loading').
158
+ // https://tanstack.com/query/v4/docs/framework/react/guides/queries
159
+ called: !(status === 'loading' && fetchStatus === 'idle'),
160
+ // 'loading' should only be true when actively fetching (fetchStatus = 'fetching') while there is no data yet (status = 'loading').
161
+ // If there is already data for the query, then 'loading' will not become 'true' when refetching, so the previous data can still be
162
+ // displayed while new data is fetched in the background
163
+ loading: fetchStatus === 'fetching' && status === 'loading',
164
+ // 'fetching' reflects the fetching behavior behind the scenes
165
+ fetching: fetchStatus === 'fetching',
166
+ error: ourError,
167
+ data,
168
+ refetch,
169
+ }
170
+ }
@@ -0,0 +1,195 @@
1
+ import { renderHook, act, waitFor } from '@testing-library/react'
2
+ import { useQueryExecutor } from './useQueryExecutor'
3
+
4
+ const testError = new Error('TEST ERROR')
5
+ let theSignal: AbortSignal | undefined
6
+ const execute = jest.fn(async ({ signal }) => {
7
+ theSignal = signal
8
+ return 42
9
+ })
10
+ const failingExecute = jest.fn(async () => {
11
+ throw testError
12
+ })
13
+
14
+ describe('useQueryExecutor', () => {
15
+ afterEach(() => {
16
+ jest.clearAllMocks()
17
+ theSignal = undefined
18
+ })
19
+ it('When not immediate, should start with called false and loading false', () => {
20
+ const { result } = renderHook(() =>
21
+ useQueryExecutor({
22
+ execute,
23
+ immediate: false,
24
+ singular: true,
25
+ variables: {},
26
+ })
27
+ )
28
+
29
+ expect(result.current).toMatchObject({
30
+ called: false,
31
+ loading: false,
32
+ })
33
+ })
34
+
35
+ it('When immediate, should start with called true and loading true', async () => {
36
+ const { result } = renderHook(() =>
37
+ useQueryExecutor({
38
+ execute,
39
+ immediate: true,
40
+ singular: true,
41
+ variables: {},
42
+ })
43
+ )
44
+
45
+ expect(result.current).toMatchObject({
46
+ called: true,
47
+ loading: true,
48
+ })
49
+
50
+ await waitFor(() => {
51
+ expect(result.current).toMatchObject({
52
+ called: true,
53
+ loading: false,
54
+ data: 42,
55
+ })
56
+ })
57
+ })
58
+
59
+ it('Should start when refetch called (if not immediate)', async () => {
60
+ const { result } = renderHook(() =>
61
+ useQueryExecutor({
62
+ execute,
63
+ immediate: false,
64
+ singular: true,
65
+ variables: {},
66
+ })
67
+ )
68
+
69
+ expect(result.current).toMatchObject({
70
+ called: false,
71
+ loading: false,
72
+ })
73
+
74
+ act(() => {
75
+ result.current.refetch()
76
+ })
77
+
78
+ expect(result.current).toMatchObject({
79
+ called: true,
80
+ loading: true,
81
+ })
82
+
83
+ await waitFor(() => {
84
+ expect(result.current).toMatchObject({
85
+ called: true,
86
+ loading: false,
87
+ data: 42,
88
+ })
89
+ })
90
+ })
91
+
92
+ it('Should report an error when execute fails', async () => {
93
+ const { result } = renderHook(() =>
94
+ useQueryExecutor({
95
+ execute: failingExecute,
96
+ immediate: false,
97
+ singular: true,
98
+ variables: {},
99
+ })
100
+ )
101
+
102
+ expect(result.current).toMatchObject({
103
+ called: false,
104
+ loading: false,
105
+ })
106
+
107
+ act(() => {
108
+ result.current.refetch()
109
+ })
110
+
111
+ expect(result.current).toMatchObject({
112
+ called: true,
113
+ loading: true,
114
+ })
115
+
116
+ await waitFor(() => {
117
+ expect(result.current).toMatchObject({
118
+ called: true,
119
+ loading: false,
120
+ error: testError,
121
+ })
122
+ })
123
+ })
124
+
125
+ it("Shouldn't abort+refetch when inputs change on subsequent renders", async () => {
126
+ const { result, rerender } = renderHook(
127
+ ({ onComplete }) =>
128
+ useQueryExecutor({
129
+ execute,
130
+ immediate: true,
131
+ singular: true,
132
+ variables: {},
133
+ onComplete,
134
+ }),
135
+ {
136
+ initialProps: { onComplete: () => null },
137
+ }
138
+ )
139
+
140
+ expect(result.current).toMatchObject({
141
+ called: true,
142
+ loading: true,
143
+ })
144
+
145
+ rerender({ onComplete: () => null })
146
+
147
+ await waitFor(() => {
148
+ expect(result.current).toMatchObject({
149
+ called: true,
150
+ loading: false,
151
+ data: 42,
152
+ })
153
+
154
+ expect(theSignal && theSignal.aborted).toBe(false)
155
+ expect(execute).toHaveBeenCalledTimes(1)
156
+ })
157
+ })
158
+
159
+ it('Should respect abort signal', async () => {
160
+ const { result } = renderHook(() =>
161
+ useQueryExecutor({
162
+ execute,
163
+ immediate: false,
164
+ singular: true,
165
+ variables: {},
166
+ })
167
+ )
168
+
169
+ expect(result.current).toMatchObject({
170
+ called: false,
171
+ loading: false,
172
+ })
173
+
174
+ act(() => {
175
+ result.current.refetch()
176
+ })
177
+
178
+ expect(result.current).toMatchObject({
179
+ called: true,
180
+ loading: true,
181
+ })
182
+
183
+ act(() => {
184
+ result.current.abort()
185
+ })
186
+
187
+ expect(theSignal && theSignal.aborted).toBe(true)
188
+
189
+ expect(result.current).toMatchObject({
190
+ called: true,
191
+ loading: false,
192
+ error: { type: 'aborted', message: 'Aborted' },
193
+ })
194
+ })
195
+ })
@@ -0,0 +1,99 @@
1
+ import { FetchError } from '@dhis2/data-engine'
2
+ import type { QueryExecuteOptions } from '@dhis2/data-engine'
3
+ import { useState, useCallback, useRef, useEffect } from 'react'
4
+ import { ExecuteHookInput, ExecuteHookResult } from '../../types'
5
+ import { useStaticInput } from './useStaticInput'
6
+
7
+ interface StateType<T> {
8
+ called: boolean
9
+ loading: boolean
10
+ error?: FetchError
11
+ data?: T
12
+ }
13
+
14
+ export const useQueryExecutor = <ReturnType>({
15
+ execute,
16
+ variables: initialVariables,
17
+ singular,
18
+ immediate,
19
+ onComplete,
20
+ onError,
21
+ }: ExecuteHookInput<ReturnType>): ExecuteHookResult<ReturnType> => {
22
+ const [theExecute] = useStaticInput(execute)
23
+ const [state, setState] = useState<StateType<ReturnType>>({
24
+ called: !!immediate,
25
+ loading: !!immediate,
26
+ })
27
+
28
+ const variables = useRef(initialVariables)
29
+
30
+ const abortControllersRef = useRef<AbortController[]>([])
31
+ const abort = useCallback(() => {
32
+ abortControllersRef.current.forEach((controller) => controller.abort())
33
+ abortControllersRef.current = []
34
+ }, [])
35
+
36
+ const manualAbort = useCallback(() => {
37
+ abort()
38
+ setState((state) => ({
39
+ called: state.called,
40
+ loading: false,
41
+ error: new FetchError({ type: 'aborted', message: 'Aborted' }),
42
+ }))
43
+ }, [abort])
44
+
45
+ const refetch = useCallback(
46
+ (newVariables = {}) => {
47
+ setState((state) =>
48
+ !state.called || !state.loading
49
+ ? { called: true, loading: true }
50
+ : state
51
+ )
52
+
53
+ if (singular) {
54
+ abort() // Cleanup any in-progress fetches
55
+ }
56
+ const controller = new AbortController()
57
+ abortControllersRef.current.push(controller)
58
+
59
+ variables.current = {
60
+ ...variables.current,
61
+ ...newVariables,
62
+ }
63
+
64
+ const options: QueryExecuteOptions = {
65
+ variables: variables.current,
66
+ signal: controller.signal,
67
+ onComplete,
68
+ onError,
69
+ }
70
+
71
+ return theExecute(options)
72
+ .then((data: ReturnType) => {
73
+ if (!controller.signal.aborted) {
74
+ setState({ called: true, loading: false, data })
75
+ return data
76
+ }
77
+ return new Promise<ReturnType>(() => undefined) // Wait forever
78
+ })
79
+ .catch((error: FetchError) => {
80
+ if (!controller.signal.aborted) {
81
+ setState({ called: true, loading: false, error })
82
+ }
83
+ return new Promise<ReturnType>(() => undefined) // Don't throw errors in refetch promises, wait forever
84
+ })
85
+ },
86
+ [abort, onComplete, onError, singular, theExecute]
87
+ )
88
+
89
+ // Don't include immediate or refetch as deps, otherwise unintentional refetches
90
+ // may be triggered by changes to input, i.e. recreating the onComplete callback
91
+ useEffect(() => {
92
+ if (immediate) {
93
+ refetch()
94
+ }
95
+ return abort
96
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
97
+
98
+ return { refetch, abort: manualAbort, ...state }
99
+ }
@@ -0,0 +1,101 @@
1
+ import { renderHook, act } from '@testing-library/react'
2
+ import { useStaticInput } from './useStaticInput'
3
+
4
+ describe('useStaticInput', () => {
5
+ const originalWarn = console.warn
6
+ const mockWarn = jest.fn()
7
+ beforeEach(() => {
8
+ jest.clearAllMocks()
9
+ console.warn = mockWarn
10
+ })
11
+ afterEach(() => (console.warn = originalWarn))
12
+
13
+ it('Should pass without warnings on first render', () => {
14
+ const { result } = renderHook(() => useStaticInput(42))
15
+ expect(result.current[0]).toBe(42)
16
+ expect(mockWarn).not.toHaveBeenCalled()
17
+ })
18
+
19
+ it('Should refuse update on updated render', () => {
20
+ const { result, rerender } = renderHook(
21
+ ({ value }) => useStaticInput(value),
22
+ { initialProps: { value: 42 } }
23
+ )
24
+ expect(result.current[0]).toBe(42)
25
+ rerender({ value: 54 })
26
+ expect(mockWarn).not.toHaveBeenCalled()
27
+ expect(result.current[0]).toBe(42)
28
+ })
29
+
30
+ it('Should show a warning on updated render (with warn prop)', () => {
31
+ const { result, rerender } = renderHook(
32
+ ({ value }) => useStaticInput(value, { warn: true }),
33
+ { initialProps: { value: 42 } }
34
+ )
35
+ expect(result.current[0]).toBe(42)
36
+ rerender({ value: 54 })
37
+ expect(mockWarn).toHaveBeenCalled()
38
+ expect(mockWarn.mock.calls.pop()).toMatchInlineSnapshot(`
39
+ Array [
40
+ "The input should be static, don't create it within the render loop!",
41
+ ]
42
+ `)
43
+ expect(result.current[0]).toBe(42)
44
+ })
45
+
46
+ it('Should show custom name in warning if supplied', () => {
47
+ const { result, rerender } = renderHook(
48
+ ({ value }) =>
49
+ useStaticInput(value, { warn: true, name: 'TESTING THING' }),
50
+ { initialProps: { value: 42 } }
51
+ )
52
+ expect(result.current[0]).toBe(42)
53
+ rerender({ value: 54 })
54
+ expect(mockWarn).toHaveBeenCalled()
55
+ expect(mockWarn.mock.calls.pop()[0]).toMatch(/TESTING THING/g)
56
+ expect(result.current[0]).toBe(42)
57
+ })
58
+
59
+ it('Should update when explicitly set to update', async () => {
60
+ const { result, rerender } = renderHook(
61
+ ({ value }) => useStaticInput(value, { warn: true }),
62
+ {
63
+ initialProps: { value: 42 },
64
+ }
65
+ )
66
+ const [value, setValue] = result.current
67
+ expect(value).toBe(42)
68
+
69
+ act(() => {
70
+ setValue(54)
71
+ })
72
+
73
+ expect(result.current[0]).toBe(54)
74
+ expect(mockWarn).not.toHaveBeenCalled()
75
+
76
+ rerender({ value: 54 })
77
+ expect(result.current[0]).toBe(54)
78
+ expect(mockWarn).toHaveBeenCalled()
79
+
80
+ rerender({ value: 75 })
81
+ expect(result.current[0]).toBe(54)
82
+ expect(mockWarn).toHaveBeenCalledTimes(2)
83
+ })
84
+
85
+ it('Should support functional values', () => {
86
+ const fn = jest.fn()
87
+ const fn2 = jest.fn()
88
+ const { result, rerender } = renderHook(
89
+ ({ value }) => useStaticInput(value, { warn: true }),
90
+ {
91
+ initialProps: { value: fn },
92
+ }
93
+ )
94
+ expect(fn).not.toHaveBeenCalled()
95
+ expect(result.current[0]).toBe(fn)
96
+ result.current[0]()
97
+ expect(fn).toHaveBeenCalled()
98
+ rerender({ value: fn2 })
99
+ expect(result.current[0]).toBe(fn)
100
+ })
101
+ })
@@ -0,0 +1,27 @@
1
+ import React, { useState, useEffect, useRef, useDebugValue } from 'react'
2
+
3
+ interface StaticInputOptions {
4
+ warn?: boolean
5
+ name?: string
6
+ }
7
+ export const useStaticInput = <T>(
8
+ staticValue: T,
9
+ { warn = false, name = 'input' }: StaticInputOptions = {}
10
+ ): [T, React.Dispatch<React.SetStateAction<T>>] => {
11
+ const originalValue = useRef(staticValue)
12
+ const [value, setValue] = useState<T>(() => originalValue.current)
13
+
14
+ useDebugValue(
15
+ value,
16
+ (debugValue) => `${name}: ${JSON.stringify(debugValue)}`
17
+ )
18
+
19
+ useEffect(() => {
20
+ if (warn && originalValue.current !== staticValue) {
21
+ console.warn(
22
+ `The ${name} should be static, don't create it within the render loop!`
23
+ )
24
+ }
25
+ }, [warn, staticValue, originalValue, name])
26
+ return [value, setValue]
27
+ }
@@ -0,0 +1,2 @@
1
+ export * from './components'
2
+ export * from './hooks'
@@ -0,0 +1,5 @@
1
+ import '@testing-library/jest-dom'
2
+
3
+ process.on('unhandledRejection', (err) => {
4
+ throw err
5
+ })
package/src/types.ts ADDED
@@ -0,0 +1,72 @@
1
+ import type {
2
+ DataEngine,
3
+ QueryExecuteOptions,
4
+ FetchError,
5
+ JsonValue,
6
+ QueryVariables,
7
+ QueryResult,
8
+ } from '@dhis2/data-engine'
9
+
10
+ export type { Mutation, Query } from '@dhis2/data-engine'
11
+
12
+ export interface ContextType {
13
+ engine: DataEngine
14
+ }
15
+
16
+ export interface ContextInput {
17
+ baseUrl: string
18
+ apiVersion: number
19
+ }
20
+
21
+ export type ExecuteOptions = QueryExecuteOptions
22
+ export type RefetchOptions = QueryVariables
23
+ export type RefetchFunction<ReturnType> = (
24
+ options?: RefetchOptions
25
+ ) => Promise<ReturnType>
26
+ export type QueryRefetchFunction = RefetchFunction<QueryResult>
27
+ export type MutationFunction = RefetchFunction<JsonValue>
28
+
29
+ export type ExecuteFunction<T> = (options: QueryExecuteOptions) => Promise<T>
30
+ export interface ExecuteHookInput<ReturnType> {
31
+ execute: ExecuteFunction<ReturnType>
32
+ variables: QueryVariables
33
+ singular: boolean
34
+ immediate: boolean
35
+ transformData?: (data: JsonValue[]) => JsonValue
36
+ onComplete?: (data: any) => void
37
+ onError?: (error: FetchError) => void
38
+ }
39
+
40
+ export interface ExecuteHookResult<ReturnType> {
41
+ refetch: RefetchFunction<ReturnType>
42
+ abort: () => void
43
+ called: boolean
44
+ loading: boolean
45
+ error?: FetchError
46
+ data?: ReturnType
47
+ }
48
+
49
+ export interface QueryState<TQueryResult> {
50
+ called: boolean
51
+ loading: boolean
52
+ fetching: boolean
53
+ error?: FetchError
54
+ data?: TQueryResult
55
+ }
56
+
57
+ export interface QueryRenderInput<
58
+ TQueryResult = QueryResult,
59
+ > extends QueryState<TQueryResult> {
60
+ engine: DataEngine
61
+ refetch: QueryRefetchFunction
62
+ }
63
+
64
+ export interface MutationState {
65
+ engine: DataEngine
66
+ called: boolean
67
+ loading: boolean
68
+ error?: FetchError
69
+ data?: JsonValue
70
+ }
71
+
72
+ export type MutationRenderInput = [MutationFunction, MutationState]
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src"],
4
+ "exclude": [
5
+ "src/setupRTL.ts",
6
+ "src/__tests__",
7
+ "**/*.test.ts",
8
+ "**/*.test.tsx"
9
+ ]
10
+ }