diff --git a/app/definitions/rest/helpers/index.ts b/app/definitions/rest/helpers/index.ts index 00a73d1ff..3ea743c00 100644 --- a/app/definitions/rest/helpers/index.ts +++ b/app/definitions/rest/helpers/index.ts @@ -91,3 +91,5 @@ export type ResultFor | SuccessResult> | FailureResult | UnauthorizedResult; + +export type ErrorResult = FailureResult; diff --git a/app/lib/hooks/useEndpointData.test.tsx b/app/lib/hooks/useEndpointData.test.tsx new file mode 100644 index 000000000..6785a80ec --- /dev/null +++ b/app/lib/hooks/useEndpointData.test.tsx @@ -0,0 +1,87 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { render, waitFor } from '@testing-library/react-native'; +import React from 'react'; +import { View, Text } from 'react-native'; + +import { useEndpointData } from './useEndpointData'; +import sdk from '../services/sdk'; + +const url = 'chat.getMessage'; + +export const message = { + _id: '9tYkmJ67wMwmvQouD', + t: 'uj', + rid: 'GENERAL', + ts: '2022-07-05T19:34:30.146Z', + msg: 'xdani', + u: { + _id: 'ombax8oEZnE7N3Mtt', + username: 'xdani', + name: 'xdani' + }, + groupable: false, + _updatedAt: '2022-07-05T19:34:30.146Z' +}; + +// mock sdk +jest.mock('../services/sdk', () => ({ + get: jest.fn(() => new Promise(resolve => setTimeout(() => resolve({ success: true, message }), 1000))) +})); + +function Render() { + const { loading } = useEndpointData(url, { msgId: message._id }); + if (loading) { + return ( + + loading + + ); + } + return ( + + load complete + + ); +} + +describe('useFetch', () => { + it('should return data after fetch', async () => { + const { result, waitForNextUpdate } = renderHook(() => useEndpointData(url, { msgId: message._id })); + expect(result.current.loading).toEqual(true); + expect(result.current.result).toEqual(undefined); + await waitForNextUpdate(); + expect(result.current.loading).toEqual(false); + expect(result.current.result).toEqual({ success: true, message }); + }); + + it('should component load correctly', async () => { + const renderComponent = render(); + const loading = await renderComponent.findByTestId('loading'); + expect(loading.props.children).toBe('loading'); + await waitFor( + () => { + expect(renderComponent.getByText('load complete')).toBeTruthy(); + }, + { timeout: 2000 } + ); + }); + + it('should return error after fetch', async () => { + const spy = jest + .spyOn(sdk, 'get') + .mockImplementation( + jest.fn(() => new Promise(resolve => setTimeout(() => resolve({ success: false, error: null }), 1000))) + ); + + const { result, waitForNextUpdate } = renderHook(() => useEndpointData(url, { msgId: message._id })); + expect(result.current.loading).toEqual(true); + expect(result.current.result).toEqual(undefined); + expect(result.current.error).toEqual(undefined); + await waitForNextUpdate(); + expect(result.current.loading).toEqual(false); + expect(result.current.result).toEqual(undefined); + expect(result.current.error).toEqual({ success: false, error: null }); + + spy.mockRestore(); + }); +}); diff --git a/app/lib/hooks/useEndpointData.ts b/app/lib/hooks/useEndpointData.ts new file mode 100644 index 000000000..dd3cab16a --- /dev/null +++ b/app/lib/hooks/useEndpointData.ts @@ -0,0 +1,62 @@ +import isEqual from 'lodash/isEqual'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { ErrorResult, MatchPathPattern, OperationParams, PathFor, ResultFor, Serialized } from '../../definitions/rest/helpers'; +import sdk from '../services/sdk'; + +export const useEndpointData = >( + endpoint: TPath, + params: void extends OperationParams<'GET', MatchPathPattern> + ? void + : Serialized>> = undefined as void extends OperationParams< + 'GET', + MatchPathPattern + > + ? void + : Serialized>> +): { + result: Serialized>> | undefined; + loading: boolean; + reload: Function; + error: ErrorResult | undefined; +} => { + const [loading, setLoading] = useState(true); + const [result, setResult] = useState>> | undefined>(); + const [error, setError] = useState(); + + const paramsRef = useRef(params); + + if (!isEqual(paramsRef.current, params)) { + paramsRef.current = params; + } + + const fetchData = useCallback(() => { + if (!endpoint) return; + setLoading(true); + sdk + .get(endpoint, params) + .then(e => { + setLoading(false); + if (e.success) { + setResult(e); + } else { + setError(e as ErrorResult); + } + }) + .catch((e: ErrorResult) => { + setLoading(false); + setError(e); + }); + }, [paramsRef.current]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { + result, + loading, + reload: fetchData, + error + }; +}; diff --git a/jest.preset.js b/jest.preset.js index 74110dd61..3d875242b 100644 --- a/jest.preset.js +++ b/jest.preset.js @@ -1,4 +1,4 @@ -const jestRN = require('react-native/jest-preset'); +const jestRN = require('@testing-library/react-native/jest-preset/index.js'); const jestExpo = require('jest-expo/jest-preset.js'); module.exports = { diff --git a/package.json b/package.json index 26c08fdc0..1effa3f50 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,7 @@ "@storybook/addon-storyshots": "5.3.21", "@storybook/react-native": "5.3.25", "@testing-library/jest-native": "^4.0.4", + "@testing-library/react-hooks": "^8.0.1", "@testing-library/react-native": "^9.0.0", "@types/bytebuffer": "^5.0.43", "@types/ejson": "^2.1.3", diff --git a/yarn.lock b/yarn.lock index 44ccb8121..a2b9f7c69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5314,6 +5314,14 @@ ramda "^0.26.1" redent "^2.0.0" +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + "@testing-library/react-native@^9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-9.0.0.tgz#e9c63411e93d2e8e70d744b12aeb78c58025c5fc" @@ -16424,6 +16432,13 @@ react-draggable@^4.0.3: classnames "^2.2.5" prop-types "^15.6.0" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.3: version "6.0.9" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"