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:
Johan Öbrink 2021-02-08 17:27:51 +01:00 committed by GitHub
parent f89f1431df
commit 9ed5df2e45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 746 additions and 33 deletions

View File

@ -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'] }],
},
}

View File

@ -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>
)

View File

@ -0,0 +1,6 @@
const reporter = {
log: jest.fn().mockName('log'),
error: jest.fn().mockName('error'),
}
export default reporter

View File

@ -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), [])

View File

@ -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)
}
})
}
}
}

View File

@ -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(() => {

View File

@ -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,

View File

@ -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

View File

@ -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')
})
})
})

View File

@ -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')
})
})
})

View File

@ -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')
})
})
})

View File

@ -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')
})
})
})

View File

@ -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')
})
})
})

View File

@ -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')
})
})
})

View File

@ -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')
})
})
})

View File

@ -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')
})
})
})