axios-hooks/test/index.test.js
Simone Busoli b8f504a1e8 feat(199): add ability to disable cache
This feature introduces the ability to disable caching
entirely when using `configure` or `makeUseAxios`,
by setting the `cache` option to false:

```
// with configure
configure({ cache: false })

// with makeUseAxios
const useAxios = makeUseAxios({ cache: false })
```

fixes #199
2020-04-11 12:59:07 +02:00

545 lines
14 KiB
JavaScript

import { renderHook, act } from '@testing-library/react-hooks'
import axios from 'axios'
import useAxiosStatic, {
configure as configureStatic,
resetConfigure as resetConfigureStatic,
makeUseAxios
} from '../src'
import { mockCancelToken } from './testUtils'
jest.mock('axios')
let cancel
let token
let errors
beforeEach(() => {
;({ cancel, token } = mockCancelToken(axios))
})
beforeAll(() => {
const error = console.error
console.error = (...args) => {
error.apply(console, args) // keep default behaviour
errors.push(args)
}
})
beforeEach(() => {
errors = []
})
afterEach(() => {
// assert that no errors were logged during tests
expect(errors.length).toBe(0)
})
describe('useAxiosStatic', () => {
standardTests(useAxiosStatic, configureStatic, resetConfigureStatic)
})
describe('makeUseAxios', () => {
it('should be a function', () => {
expect(makeUseAxios).toBeInstanceOf(Function)
})
it('should not throw', () => {
expect(makeUseAxios({})).toBeTruthy()
})
it('should provide a custom implementation of axios', () => {
const mockAxios = jest.fn().mockResolvedValueOnce({ data: 'whatever' })
const useAxios = makeUseAxios({ axios: mockAxios })
const { waitForNextUpdate } = renderHook(() => useAxios(''))
expect(mockAxios).toHaveBeenCalled()
return waitForNextUpdate()
})
it('should allow to disable cache', async () => {
const useAxios = makeUseAxios({ cache: false })
axios.mockResolvedValueOnce({ data: 'whatever' })
const { waitForNextUpdate } = renderHook(() => useAxios(''))
await waitForNextUpdate()
expect(axios).toHaveBeenCalledTimes(1)
expect(axios).toHaveBeenCalledWith(
expect.not.objectContaining({ adapter: expect.any(Function) })
)
})
describe('standard tests', () => {
const useAxios = makeUseAxios({})
standardTests(useAxios, useAxios.configure, useAxios.resetConfigure)
describe('with custom configuration', () => {
const useAxios = makeUseAxios({ axios })
standardTests(useAxios, useAxios.configure, useAxios.resetConfigure)
})
})
})
function standardTests(useAxios, configure, resetConfigure) {
describe('basic functionality', () => {
it('should set loading to true', async () => {
axios.mockResolvedValueOnce({ data: 'whatever' })
const { result, waitForNextUpdate } = renderHook(() => useAxios(''))
expect(result.current[0].loading).toBe(true)
await waitForNextUpdate()
})
it('should set loading to false when request completes and returns data', async () => {
axios.mockResolvedValueOnce({ data: 'whatever' })
const { result, waitForNextUpdate } = renderHook(() => useAxios(''))
await waitForNextUpdate()
expect(result.current[0].loading).toBe(false)
expect(result.current[0].data).toBe('whatever')
})
it('should set the response', async () => {
const response = { data: 'whatever' }
axios.mockResolvedValueOnce(response)
const { result, waitForNextUpdate } = renderHook(() => useAxios(''))
await waitForNextUpdate()
expect(result.current[0].loading).toBe(false)
expect(result.current[0].response).toBe(response)
})
it('should reset error when request completes and returns data', async () => {
const error = new Error('boom')
axios.mockRejectedValueOnce(error)
const { result, waitForNextUpdate } = renderHook(() => useAxios(''))
await waitForNextUpdate()
expect(result.current[0].error).toBe(error)
axios.mockResolvedValueOnce({ data: 'whatever' })
// Refetch
act(() => {
result.current[1]()
})
await waitForNextUpdate()
expect(result.current[0].error).toBe(null)
})
it('should set loading to false when request completes and returns error', async () => {
const error = new Error('boom')
axios.mockRejectedValueOnce(error)
const { result, waitForNextUpdate } = renderHook(() => useAxios(''))
await waitForNextUpdate()
expect(result.current[0].loading).toBe(false)
expect(result.current[0].error).toBe(error)
})
it('should refetch', async () => {
axios.mockResolvedValue({ data: 'whatever' })
const { result, waitForNextUpdate } = renderHook(() => useAxios(''))
await waitForNextUpdate()
act(() => {
result.current[1]()
})
expect(result.current[0].loading).toBe(true)
expect(axios).toHaveBeenCalledTimes(2)
await waitForNextUpdate()
})
it('should return the same reference to the fetch function', async () => {
axios.mockResolvedValue({ data: 'whatever' })
const { result, rerender, waitForNextUpdate } = renderHook(() =>
useAxios('')
)
const firstRefetch = result.current[1]
rerender()
expect(result.current[1]).toBe(firstRefetch)
await waitForNextUpdate()
})
})
describe('request cancellation', () => {
describe('effect-generated requests', () => {
it('should provide the cancel token to axios', async () => {
axios.mockResolvedValueOnce({ data: 'whatever' })
const { waitForNextUpdate } = renderHook(() => useAxios(''))
expect(axios).toHaveBeenCalledWith(
expect.objectContaining({
cancelToken: token
})
)
await waitForNextUpdate()
})
it('should cancel the outstanding request when the component unmounts', async () => {
axios.mockResolvedValueOnce({ data: 'whatever' })
const { waitForNextUpdate, unmount } = renderHook(() => useAxios(''))
await waitForNextUpdate()
unmount()
expect(cancel).toHaveBeenCalled()
})
it('should cancel the outstanding request when the component refetches due to a rerender', async () => {
axios.mockResolvedValue({ data: 'whatever' })
const { waitForNextUpdate, rerender } = renderHook(
props => useAxios(props),
{ initialProps: 'initial config' }
)
await waitForNextUpdate()
rerender('new config')
expect(cancel).toHaveBeenCalled()
await waitForNextUpdate()
})
it('should not cancel the outstanding request when the component rerenders with same config', async () => {
axios.mockResolvedValue({ data: 'whatever' })
const { waitForNextUpdate, rerender } = renderHook(
props => useAxios(props),
{ initialProps: 'initial config' }
)
await waitForNextUpdate()
rerender()
expect(cancel).not.toHaveBeenCalled()
})
it('should not dispatch an error when the request is canceled', async () => {
const cancellation = new Error('canceled')
axios.mockRejectedValueOnce(cancellation)
axios.isCancel = jest
.fn()
.mockImplementationOnce(err => err === cancellation)
const { result, wait } = renderHook(() => useAxios(''))
// if we cancel we won't dispatch the error, hence there's no state update
// to wait for. yet, if we don't try to wait, we won't know if we're handling
// the error properly because the return value will not have the error until a
// state update happens. it would be great to have a better way to test this
await wait(
() => {
expect(result.current[0].error).toBeNull()
},
{ timeout: 1000, suppressErrors: false }
)
})
})
describe('manual refetches', () => {
it('should provide the cancel token to axios', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useAxios('', { manual: true })
)
axios.mockResolvedValueOnce({ data: 'whatever' })
act(() => {
result.current[1]()
})
expect(axios).toHaveBeenCalledTimes(1)
expect(axios).toHaveBeenLastCalledWith(
expect.objectContaining({
cancelToken: token
})
)
await waitForNextUpdate()
})
it('should cancel the outstanding manual refetch when the component unmounts', async () => {
const { result, waitForNextUpdate, unmount } = renderHook(() =>
useAxios('', { manual: true })
)
axios.mockResolvedValueOnce({ data: 'whatever' })
act(() => {
result.current[1]()
})
await waitForNextUpdate()
unmount()
expect(cancel).toHaveBeenCalled()
})
it('should cancel the outstanding manual refetch when the component refetches', async () => {
axios.mockResolvedValue({ data: 'whatever' })
const { result, waitForNextUpdate, rerender } = renderHook(
config => useAxios(config),
{ initialProps: '' }
)
act(() => {
result.current[1]()
})
await waitForNextUpdate()
rerender('new config')
expect(cancel).toHaveBeenCalled()
await waitForNextUpdate()
})
})
})
describe('refetch', () => {
describe('when axios resolves', () => {
it('should resolve to the response by default', async () => {
const response = { data: 'whatever' }
axios.mockResolvedValue(response)
const {
result: {
current: [, refetch]
},
waitForNextUpdate
} = renderHook(() => useAxios(''))
act(() => {
expect(refetch()).resolves.toEqual(response)
})
await waitForNextUpdate()
})
it('should resolve to the response when using cache', async () => {
const response = { data: 'whatever' }
axios.mockResolvedValue(response)
const {
result: {
current: [, refetch]
},
waitForNextUpdate
} = renderHook(() => useAxios(''))
act(() => {
expect(refetch({}, { useCache: true })).resolves.toEqual(response)
})
await waitForNextUpdate()
})
})
describe('when axios rejects', () => {
it('should reject with the error by default', async () => {
const error = new Error('boom')
axios.mockRejectedValue(error)
const {
result: {
current: [, refetch]
},
waitForNextUpdate
} = renderHook(() => useAxios(''))
act(() => {
expect(refetch()).rejects.toEqual(error)
})
await waitForNextUpdate()
})
it('should reject with the error when using cache', async () => {
const error = new Error('boom')
axios.mockRejectedValue(error)
const {
result: {
current: [, refetch]
},
waitForNextUpdate
} = renderHook(() => useAxios(''))
act(() => {
expect(refetch({}, { useCache: true })).rejects.toEqual(error)
})
await waitForNextUpdate()
})
})
})
describe('manual option', () => {
it('should set loading to false', async () => {
const { result } = renderHook(() => useAxios('', { manual: true }))
expect(result.current[0].loading).toBe(false)
})
it('should not execute request', () => {
renderHook(() => useAxios('', { manual: true }))
expect(axios).not.toHaveBeenCalled()
})
it('should execute request manually and skip cache by default', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useAxios('', { manual: true })
)
axios.mockResolvedValueOnce({ data: 'whatever' })
act(() => {
result.current[1]()
})
expect(result.current[0].loading).toBe(true)
await waitForNextUpdate()
expect(result.current[0].loading).toBe(false)
expect(result.current[0].data).toBe('whatever')
expect(axios).toHaveBeenCalledTimes(1)
expect(axios).toHaveBeenCalledWith(
expect.not.objectContaining({ adapter: expect.any(Function) })
)
})
it('should allow using the cache', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useAxios('', { manual: true })
)
axios.mockResolvedValueOnce({ data: 'whatever' })
act(() => {
result.current[1]({}, { useCache: true })
})
await waitForNextUpdate()
expect(axios).toHaveBeenCalledTimes(1)
expect(axios).toHaveBeenCalledWith(
expect.objectContaining({ adapter: expect.any(Function) })
)
})
})
describe('useCache option', () => {
it('should use cache by default', async () => {
axios.mockResolvedValueOnce({ data: 'whatever' })
const { waitForNextUpdate } = renderHook(() => useAxios(''))
await waitForNextUpdate()
expect(axios).toHaveBeenCalledTimes(1)
expect(axios).toHaveBeenCalledWith(
expect.objectContaining({ adapter: expect.any(Function) })
)
})
it('should allow disabling cache', async () => {
axios.mockResolvedValueOnce({ data: 'whatever' })
const { waitForNextUpdate } = renderHook(() =>
useAxios('', { useCache: false })
)
await waitForNextUpdate()
expect(axios).toHaveBeenCalledTimes(1)
expect(axios).toHaveBeenCalledWith(
expect.not.objectContaining({ adapter: expect.any(Function) })
)
})
})
describe('configure', () => {
afterEach(() => resetConfigure())
it('should provide a custom implementation of axios', () => {
const mockAxios = jest.fn().mockReturnValueOnce({ data: 'whatever' })
configure({ axios: mockAxios })
const { waitForNextUpdate } = renderHook(() => useAxios(''))
expect(mockAxios).toHaveBeenCalled()
return waitForNextUpdate()
})
it('should allow to disable cache', async () => {
configure({ cache: false })
axios.mockResolvedValueOnce({ data: 'whatever' })
const { waitForNextUpdate } = renderHook(() => useAxios(''))
await waitForNextUpdate()
expect(axios).toHaveBeenCalledTimes(1)
expect(axios).toHaveBeenCalledWith(
expect.not.objectContaining({ adapter: expect.any(Function) })
)
})
})
}