feat: 🎸 API call retries and support for error reporting (#5)
* feat: 🎸 API call retries and support for error reporting * docs: ✏️ Added reporter to README
This commit is contained in:
parent
f89f1431df
commit
9ed5df2e45
|
@ -10,6 +10,6 @@ module.exports = {
|
|||
'@typescript-eslint/semi': ['error', 'never'],
|
||||
'jest/no-mocks-import': [0],
|
||||
'max-len': [1, 110],
|
||||
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
|
||||
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.tsx'] }],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -23,11 +23,16 @@ import init from '@skolplattformen/embedded-api'
|
|||
import { CookieManager } from '@react-native-community/cookies'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { RootComponent } from './components/root'
|
||||
import crashlytics from '@react-native-firebase/crashlytics'
|
||||
|
||||
const api = init(fetch, () => CookieManager.clearAll())
|
||||
const reporter = {
|
||||
log: (message) => crashlytics().log(message),
|
||||
error: (error, label) => crashlytics().recordError(error, label),
|
||||
}
|
||||
|
||||
export default () => (
|
||||
<ApiProvider api={api} storage={AsyncStorage}>
|
||||
<ApiProvider api={api} reporter={reporter} storage={AsyncStorage}>
|
||||
<RootComponent />
|
||||
</ApiProvider>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
const reporter = {
|
||||
log: jest.fn().mockName('log'),
|
||||
error: jest.fn().mockName('error'),
|
||||
}
|
||||
|
||||
export default reporter
|
10
src/hooks.ts
10
src/hooks.ts
|
@ -39,7 +39,9 @@ const hook = <T>(
|
|||
const state = stateMap[key] || { status: 'pending', data: defaultValue }
|
||||
return state
|
||||
}
|
||||
const { api, storage, isLoggedIn } = useApi()
|
||||
const {
|
||||
api, isLoggedIn, reporter, storage,
|
||||
} = useApi()
|
||||
const initialState = select(store.getState() as EntityStoreRootState)
|
||||
const [state, setState] = useState(initialState)
|
||||
const dispatch = useDispatch()
|
||||
|
@ -50,6 +52,7 @@ const hook = <T>(
|
|||
key,
|
||||
defaultValue,
|
||||
apiCall: apiCaller(api),
|
||||
retries: 0,
|
||||
}
|
||||
|
||||
// Only use cache when not in fake mode
|
||||
|
@ -72,6 +75,11 @@ const hook = <T>(
|
|||
|| newState.data !== state.data
|
||||
|| newState.error !== state.error) {
|
||||
setState(newState)
|
||||
|
||||
if (newState.error) {
|
||||
const description = `Error getting ${entityName} from API`
|
||||
reporter.error(newState.error, description)
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => store.subscribe(listener), [])
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
/* eslint-disable default-case */
|
||||
import { Middleware } from 'redux'
|
||||
import { EntityAction, EntityStoreRootState } from './types'
|
||||
import { EntityAction, EntityStoreRootState, ExtraActionProps } from './types'
|
||||
|
||||
type IMiddleware = Middleware<{}, EntityStoreRootState>
|
||||
export const apiMiddleware: IMiddleware = (storeApi) => (next) => (action: EntityAction<any>) => {
|
||||
try {
|
||||
switch (action.type) {
|
||||
case 'GET_FROM_API': {
|
||||
// Retrieve from cache
|
||||
if (action.extra?.getFromCache) {
|
||||
const cacheAction: EntityAction<any> = {
|
||||
...action,
|
||||
type: 'GET_FROM_CACHE',
|
||||
}
|
||||
storeApi.dispatch(cacheAction)
|
||||
}
|
||||
|
||||
// Call api
|
||||
const apiCall = action.extra?.apiCall
|
||||
if (apiCall) {
|
||||
const extra = action.extra as ExtraActionProps<any>
|
||||
apiCall()
|
||||
.then((res: any) => {
|
||||
const resultAction: EntityAction<any> = {
|
||||
|
@ -19,7 +29,7 @@ export const apiMiddleware: IMiddleware = (storeApi) => (next) => (action: Entit
|
|||
}
|
||||
storeApi.dispatch(resultAction)
|
||||
|
||||
if (action.extra?.saveToCache && res) {
|
||||
if (extra.saveToCache && res) {
|
||||
const cacheAction: EntityAction<any> = {
|
||||
...resultAction,
|
||||
type: 'STORE_IN_CACHE',
|
||||
|
@ -27,15 +37,35 @@ export const apiMiddleware: IMiddleware = (storeApi) => (next) => (action: Entit
|
|||
storeApi.dispatch(cacheAction)
|
||||
}
|
||||
})
|
||||
}
|
||||
.catch((error) => {
|
||||
const retries = extra.retries + 1
|
||||
|
||||
// Retrieve from cache
|
||||
if (action.extra?.getFromCache) {
|
||||
const cacheAction: EntityAction<any> = {
|
||||
...action,
|
||||
type: 'GET_FROM_CACHE',
|
||||
}
|
||||
storeApi.dispatch(cacheAction)
|
||||
const errorAction: EntityAction<any> = {
|
||||
...action,
|
||||
extra: {
|
||||
...extra,
|
||||
retries,
|
||||
},
|
||||
type: 'API_ERROR',
|
||||
error,
|
||||
}
|
||||
storeApi.dispatch(errorAction)
|
||||
|
||||
// Retry 3 times
|
||||
if (retries < 3) {
|
||||
const retryAction: EntityAction<any> = {
|
||||
...action,
|
||||
type: 'GET_FROM_API',
|
||||
extra: {
|
||||
...extra,
|
||||
retries,
|
||||
},
|
||||
}
|
||||
setTimeout(() => {
|
||||
storeApi.dispatch(retryAction)
|
||||
}, retries * 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,20 @@ import React, {
|
|||
import { Provider } from 'react-redux'
|
||||
import { ApiContext } from './context'
|
||||
import store from './store'
|
||||
import { AsyncStorage, IApiContext } from './types'
|
||||
import { AsyncStorage, IApiContext, Reporter } from './types'
|
||||
|
||||
type TApiProvider = FC<PropsWithChildren<{ api: Api, storage: AsyncStorage }>>
|
||||
export const ApiProvider: TApiProvider = ({ children, api, storage }) => {
|
||||
type TApiProvider = FC<PropsWithChildren<{
|
||||
api: Api,
|
||||
storage: AsyncStorage,
|
||||
reporter?: Reporter
|
||||
}>>
|
||||
const noopReporter: Reporter = {
|
||||
log: () => { },
|
||||
error: () => { },
|
||||
}
|
||||
export const ApiProvider: TApiProvider = ({
|
||||
children, api, storage, reporter = noopReporter,
|
||||
}) => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(api.isLoggedIn)
|
||||
const [isFake, setIsFake] = useState(api.isFake)
|
||||
|
||||
|
@ -19,6 +29,7 @@ export const ApiProvider: TApiProvider = ({ children, api, storage }) => {
|
|||
storage,
|
||||
isLoggedIn,
|
||||
isFake,
|
||||
reporter,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -37,6 +37,14 @@ const createReducer = <T>(entity: EntityName): EntityReducer<T> => {
|
|||
}
|
||||
break
|
||||
}
|
||||
case 'API_ERROR': {
|
||||
newNode = {
|
||||
...node,
|
||||
status: action.extra.retries < 3 ? node.status : 'error',
|
||||
error: action.error,
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'RESULT_FROM_CACHE': {
|
||||
newNode = {
|
||||
...node,
|
||||
|
|
31
src/types.ts
31
src/types.ts
|
@ -11,11 +11,17 @@ import {
|
|||
} from '@skolplattformen/embedded-api'
|
||||
import { Action, Reducer } from 'redux'
|
||||
|
||||
export interface Reporter {
|
||||
log: (message: string) => void
|
||||
error: (error: Error, label?: string) => void
|
||||
}
|
||||
|
||||
export interface IApiContext {
|
||||
api: Api
|
||||
storage: AsyncStorage
|
||||
isLoggedIn: boolean
|
||||
isFake: boolean
|
||||
reporter: Reporter
|
||||
}
|
||||
|
||||
export type EntityStatus = 'pending' | 'loading' | 'loaded' | 'error'
|
||||
|
@ -30,21 +36,28 @@ export interface ApiCall<T> {
|
|||
}
|
||||
export interface ExtraActionProps<T> {
|
||||
apiCall: ApiCall<T>
|
||||
retries: number
|
||||
key: string
|
||||
defaultValue: T
|
||||
getFromCache?: () => Promise<string | null>
|
||||
saveToCache?: (value: string) => Promise<void>
|
||||
}
|
||||
export type EntityActionType = 'GET_FROM_API' | 'RESULT_FROM_API' | 'GET_FROM_CACHE' | 'RESULT_FROM_CACHE' | 'STORE_IN_CACHE' | 'CLEAR'
|
||||
export type EntityActionType = 'GET_FROM_API'
|
||||
| 'RESULT_FROM_API'
|
||||
| 'API_ERROR'
|
||||
| 'GET_FROM_CACHE'
|
||||
| 'RESULT_FROM_CACHE'
|
||||
| 'STORE_IN_CACHE'
|
||||
| 'CLEAR'
|
||||
export type EntityName = 'USER'
|
||||
| 'CHILDREN'
|
||||
| 'CALENDAR'
|
||||
| 'CLASSMATES'
|
||||
| 'MENU'
|
||||
| 'NEWS'
|
||||
| 'NOTIFICATIONS'
|
||||
| 'SCHEDULE'
|
||||
| 'ALL'
|
||||
| 'CHILDREN'
|
||||
| 'CALENDAR'
|
||||
| 'CLASSMATES'
|
||||
| 'MENU'
|
||||
| 'NEWS'
|
||||
| 'NOTIFICATIONS'
|
||||
| 'SCHEDULE'
|
||||
| 'ALL'
|
||||
export interface EntityAction<T> extends Action<EntityActionType> {
|
||||
entity: EntityName
|
||||
data?: T
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useCalendar } from './hooks'
|
|||
import store from './store'
|
||||
import init from './__mocks__/@skolplattformen/embedded-api'
|
||||
import createStorage from './__mocks__/AsyncStorage'
|
||||
import reporter from './__mocks__/reporter'
|
||||
|
||||
const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
|
@ -14,7 +15,13 @@ describe('useCalendar(child)', () => {
|
|||
let response
|
||||
let child
|
||||
const wrapper = ({ children }) => (
|
||||
<ApiProvider api={api} storage={storage}>{children}</ApiProvider>
|
||||
<ApiProvider
|
||||
api={api}
|
||||
storage={storage}
|
||||
reporter={reporter}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
)
|
||||
beforeEach(() => {
|
||||
response = [{ id: 1 }]
|
||||
|
@ -144,4 +151,76 @@ describe('useCalendar(child)', () => {
|
|||
expect(storage.cache.calendar_10).toEqual('[{"id":2}]')
|
||||
})
|
||||
})
|
||||
it('retries if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getCalendar.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useCalendar(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.status).toEqual('loaded')
|
||||
expect(result.current.data).toEqual([{ id: 1 }])
|
||||
})
|
||||
})
|
||||
it('gives up after 3 retries', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getCalendar.mockRejectedValueOnce(error)
|
||||
api.getCalendar.mockRejectedValueOnce(error)
|
||||
api.getCalendar.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useCalendar(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('error')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
})
|
||||
})
|
||||
it('reports if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getCalendar.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useCalendar(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
|
||||
expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting CALENDAR from API')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useChildList } from './hooks'
|
|||
import store from './store'
|
||||
import init from './__mocks__/@skolplattformen/embedded-api'
|
||||
import createStorage from './__mocks__/AsyncStorage'
|
||||
import reporter from './__mocks__/reporter'
|
||||
|
||||
const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
|
@ -13,7 +14,13 @@ describe('useChildList()', () => {
|
|||
let storage
|
||||
let response
|
||||
const wrapper = ({ children }) => (
|
||||
<ApiProvider api={api} storage={storage}>{children}</ApiProvider>
|
||||
<ApiProvider
|
||||
api={api}
|
||||
storage={storage}
|
||||
reporter={reporter}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
)
|
||||
beforeEach(() => {
|
||||
response = [{ id: 1 }]
|
||||
|
@ -130,4 +137,76 @@ describe('useChildList()', () => {
|
|||
expect(storage.cache.children).toEqual('[{"id":2}]')
|
||||
})
|
||||
})
|
||||
it('retries if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getChildren.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.status).toEqual('loaded')
|
||||
expect(result.current.data).toEqual([{ id: 1 }])
|
||||
})
|
||||
})
|
||||
it('gives up after 3 retries', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getChildren.mockRejectedValueOnce(error)
|
||||
api.getChildren.mockRejectedValueOnce(error)
|
||||
api.getChildren.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('error')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
})
|
||||
})
|
||||
it('reports if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getChildren.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
|
||||
expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting CHILDREN from API')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useClassmates } from './hooks'
|
|||
import store from './store'
|
||||
import init from './__mocks__/@skolplattformen/embedded-api'
|
||||
import createStorage from './__mocks__/AsyncStorage'
|
||||
import reporter from './__mocks__/reporter'
|
||||
|
||||
const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
|
@ -14,7 +15,13 @@ describe('useClassmates(child)', () => {
|
|||
let response
|
||||
let child
|
||||
const wrapper = ({ children }) => (
|
||||
<ApiProvider api={api} storage={storage}>{children}</ApiProvider>
|
||||
<ApiProvider
|
||||
api={api}
|
||||
storage={storage}
|
||||
reporter={reporter}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
)
|
||||
beforeEach(() => {
|
||||
response = [{ id: 1 }]
|
||||
|
@ -132,4 +139,76 @@ describe('useClassmates(child)', () => {
|
|||
expect(storage.cache.classmates_10).toEqual('[{"id":2}]')
|
||||
})
|
||||
})
|
||||
it('retries if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getClassmates.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useClassmates(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.status).toEqual('loaded')
|
||||
expect(result.current.data).toEqual([{ id: 1 }])
|
||||
})
|
||||
})
|
||||
it('gives up after 3 retries', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getClassmates.mockRejectedValueOnce(error)
|
||||
api.getClassmates.mockRejectedValueOnce(error)
|
||||
api.getClassmates.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useClassmates(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('error')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
})
|
||||
})
|
||||
it('reports if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getClassmates.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useClassmates(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
|
||||
expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting CLASSMATES from API')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useMenu } from './hooks'
|
|||
import store from './store'
|
||||
import init from './__mocks__/@skolplattformen/embedded-api'
|
||||
import createStorage from './__mocks__/AsyncStorage'
|
||||
import reporter from './__mocks__/reporter'
|
||||
|
||||
const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
|
@ -14,7 +15,13 @@ describe('useMenu(child)', () => {
|
|||
let response
|
||||
let child
|
||||
const wrapper = ({ children }) => (
|
||||
<ApiProvider api={api} storage={storage}>{children}</ApiProvider>
|
||||
<ApiProvider
|
||||
api={api}
|
||||
storage={storage}
|
||||
reporter={reporter}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
)
|
||||
beforeEach(() => {
|
||||
response = [{ id: 1 }]
|
||||
|
@ -132,4 +139,76 @@ describe('useMenu(child)', () => {
|
|||
expect(storage.cache.menu_10).toEqual('[{"id":2}]')
|
||||
})
|
||||
})
|
||||
it('retries if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getMenu.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useMenu(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.status).toEqual('loaded')
|
||||
expect(result.current.data).toEqual([{ id: 1 }])
|
||||
})
|
||||
})
|
||||
it('gives up after 3 retries', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getMenu.mockRejectedValueOnce(error)
|
||||
api.getMenu.mockRejectedValueOnce(error)
|
||||
api.getMenu.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useMenu(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('error')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
})
|
||||
})
|
||||
it('reports if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getMenu.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useMenu(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
|
||||
expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting MENU from API')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useNews } from './hooks'
|
|||
import store from './store'
|
||||
import init from './__mocks__/@skolplattformen/embedded-api'
|
||||
import createStorage from './__mocks__/AsyncStorage'
|
||||
import reporter from './__mocks__/reporter'
|
||||
|
||||
const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
|
@ -14,7 +15,13 @@ describe('useNews(child)', () => {
|
|||
let response
|
||||
let child
|
||||
const wrapper = ({ children }) => (
|
||||
<ApiProvider api={api} storage={storage}>{children}</ApiProvider>
|
||||
<ApiProvider
|
||||
api={api}
|
||||
storage={storage}
|
||||
reporter={reporter}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
)
|
||||
beforeEach(() => {
|
||||
response = [{ id: 1 }]
|
||||
|
@ -132,4 +139,76 @@ describe('useNews(child)', () => {
|
|||
expect(storage.cache.news_10).toEqual('[{"id":2}]')
|
||||
})
|
||||
})
|
||||
it('retries if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getNews.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNews(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.status).toEqual('loaded')
|
||||
expect(result.current.data).toEqual([{ id: 1 }])
|
||||
})
|
||||
})
|
||||
it('gives up after 3 retries', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getNews.mockRejectedValueOnce(error)
|
||||
api.getNews.mockRejectedValueOnce(error)
|
||||
api.getNews.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNews(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('error')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
})
|
||||
})
|
||||
it('reports if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getNews.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNews(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
|
||||
expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting NEWS from API')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useNotifications } from './hooks'
|
|||
import store from './store'
|
||||
import init from './__mocks__/@skolplattformen/embedded-api'
|
||||
import createStorage from './__mocks__/AsyncStorage'
|
||||
import reporter from './__mocks__/reporter'
|
||||
|
||||
const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
|
@ -14,7 +15,13 @@ describe('useNotifications(child)', () => {
|
|||
let response
|
||||
let child
|
||||
const wrapper = ({ children }) => (
|
||||
<ApiProvider api={api} storage={storage}>{children}</ApiProvider>
|
||||
<ApiProvider
|
||||
api={api}
|
||||
storage={storage}
|
||||
reporter={reporter}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
)
|
||||
beforeEach(() => {
|
||||
response = [{ id: 1 }]
|
||||
|
@ -132,4 +139,76 @@ describe('useNotifications(child)', () => {
|
|||
expect(storage.cache.notifications_10).toEqual('[{"id":2}]')
|
||||
})
|
||||
})
|
||||
it('retries if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getNotifications.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNotifications(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.status).toEqual('loaded')
|
||||
expect(result.current.data).toEqual([{ id: 1 }])
|
||||
})
|
||||
})
|
||||
it('gives up after 3 retries', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getNotifications.mockRejectedValueOnce(error)
|
||||
api.getNotifications.mockRejectedValueOnce(error)
|
||||
api.getNotifications.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNotifications(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('error')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
})
|
||||
})
|
||||
it('reports if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getNotifications.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNotifications(child), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
|
||||
expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting NOTIFICATIONS from API')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useSchedule } from './hooks'
|
|||
import store from './store'
|
||||
import init from './__mocks__/@skolplattformen/embedded-api'
|
||||
import createStorage from './__mocks__/AsyncStorage'
|
||||
import reporter from './__mocks__/reporter'
|
||||
|
||||
const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
|
@ -16,7 +17,13 @@ describe('useSchedule(child, from, to)', () => {
|
|||
let from
|
||||
let to
|
||||
const wrapper = ({ children }) => (
|
||||
<ApiProvider api={api} storage={storage}>{children}</ApiProvider>
|
||||
<ApiProvider
|
||||
api={api}
|
||||
storage={storage}
|
||||
reporter={reporter}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
)
|
||||
beforeEach(() => {
|
||||
response = [{ id: 1 }]
|
||||
|
@ -136,4 +143,76 @@ describe('useSchedule(child, from, to)', () => {
|
|||
expect(storage.cache['schedule_10_2021-01-01_2021-01-08']).toEqual('[{"id":2}]')
|
||||
})
|
||||
})
|
||||
it('retries if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getSchedule.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSchedule(child, from, to), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.status).toEqual('loaded')
|
||||
expect(result.current.data).toEqual([{ id: 1 }])
|
||||
})
|
||||
})
|
||||
it('gives up after 3 retries', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getSchedule.mockRejectedValueOnce(error)
|
||||
api.getSchedule.mockRejectedValueOnce(error)
|
||||
api.getSchedule.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSchedule(child, from, to), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('error')
|
||||
expect(result.current.data).toEqual([{ id: 2 }])
|
||||
})
|
||||
})
|
||||
it('reports if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getSchedule.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSchedule(child, from, to), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
|
||||
expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting SCHEDULE from API')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useUser } from './hooks'
|
|||
import store from './store'
|
||||
import init from './__mocks__/@skolplattformen/embedded-api'
|
||||
import createStorage from './__mocks__/AsyncStorage'
|
||||
import reporter from './__mocks__/reporter'
|
||||
|
||||
const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
|
@ -13,7 +14,13 @@ describe('useUser()', () => {
|
|||
let storage
|
||||
let response
|
||||
const wrapper = ({ children }) => (
|
||||
<ApiProvider api={api} storage={storage}>{children}</ApiProvider>
|
||||
<ApiProvider
|
||||
api={api}
|
||||
storage={storage}
|
||||
reporter={reporter}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
)
|
||||
beforeEach(() => {
|
||||
response = { id: 1 }
|
||||
|
@ -130,4 +137,76 @@ describe('useUser()', () => {
|
|||
expect(storage.cache.user).toEqual('{"id":2}')
|
||||
})
|
||||
})
|
||||
it('retries if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getUser.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useUser(), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual({ id: 2 })
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.status).toEqual('loaded')
|
||||
expect(result.current.data).toEqual({ id: 1 })
|
||||
})
|
||||
})
|
||||
it('gives up after 3 retries', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getUser.mockRejectedValueOnce(error)
|
||||
api.getUser.mockRejectedValueOnce(error)
|
||||
api.getUser.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useUser(), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('loading')
|
||||
expect(result.current.data).toEqual({ id: 2 })
|
||||
|
||||
jest.advanceTimersToNextTimer()
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
expect(result.current.status).toEqual('error')
|
||||
expect(result.current.data).toEqual({ id: 2 })
|
||||
})
|
||||
})
|
||||
it('reports if api fails', async () => {
|
||||
await act(async () => {
|
||||
api.isLoggedIn = true
|
||||
const error = new Error('fail')
|
||||
api.getUser.mockRejectedValueOnce(error)
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useUser(), { wrapper })
|
||||
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.error).toEqual(error)
|
||||
|
||||
expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting USER from API')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue