Last active
January 11, 2022 15:07
-
-
Save rexebin/71416657c6842d756fb732dad43932df to your computer and use it in GitHub Desktop.
Testing a Error Handler Utility Hook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { act } from '@testing-library/react-hooks'; | |
import axios, { AxiosError } from 'axios'; | |
import { useErrorHandler } from './useErrorHandler'; | |
import { MessageProvider, useMessage } from '../message/MessageContext'; | |
import { localStorageKey } from './useClient'; | |
import { render } from '@testing-library/react'; | |
import userEvent from '@testing-library/user-event'; | |
import { environment } from '@epic/environments'; | |
jest.mock('axios'); | |
jest.mock('@epic/environments'); | |
afterAll(() => { | |
jest.clearAllMocks(); | |
}); | |
function TestComponent() { | |
const { errorHandler } = useErrorHandler({ message: 'Oops!' }); | |
const sendRequest = async () => { | |
try { | |
await axios.get('/test'); | |
} catch (e) { | |
errorHandler(e as AxiosError); | |
} | |
}; | |
const { message } = useMessage(); | |
return ( | |
<div> | |
<button onClick={() => sendRequest()}>Send Request</button> | |
{message?.message ? <div data-testid={'error-message'}>{message?.message}</div> : null} | |
{message?.type ? <div data-testid={'error-type'}>{message?.type}</div> : null} | |
</div> | |
); | |
} | |
function setup() { | |
const mockedAxios = axios as jest.Mocked<typeof axios>; | |
const queries = render( | |
<MessageProvider> | |
<TestComponent /> | |
</MessageProvider> | |
); | |
return { ...queries, mockedAxios }; | |
} | |
describe('Error Handler', function () { | |
const token = 'I am a token'; | |
beforeEach(() => { | |
localStorage.setItem(localStorageKey, token); | |
}); | |
it('should remove token when authentication is failed', async function () { | |
const { getByText, findByTestId, mockedAxios } = setup(); | |
mockedAxios.get.mockRejectedValueOnce({ | |
response: { | |
status: 401, | |
statusText: 'Unauthorized', | |
data: { | |
message: 'Unauthorized', | |
}, | |
}, | |
} as AxiosError); | |
act(() => { | |
userEvent.click(getByText(/Send Request/i)); | |
}); | |
const message = await findByTestId('error-message'); | |
const type = await findByTestId('error-type'); | |
expect(message.innerHTML).toMatch(/login failed/i); | |
expect(type.innerHTML).toBe('error'); | |
expect(localStorage.getItem(localStorageKey)).toBe(null); | |
}); | |
it('should alert to login in to an account with permission when authorisation fails', async function () { | |
const { getByText, findByTestId, mockedAxios } = setup(); | |
mockedAxios.get.mockRejectedValueOnce({ | |
response: { | |
status: 403, | |
statusText: 'Unauthorized', | |
data: { | |
message: 'Unauthorized', | |
}, | |
}, | |
} as AxiosError); | |
act(() => { | |
userEvent.click(getByText(/Send Request/i)); | |
}); | |
const message = await findByTestId('error-message'); | |
const type = await findByTestId('error-type'); | |
expect(message.innerHTML).toContain('sign in with an account with the required permissions'); | |
expect(type.innerHTML).toBe('error'); | |
expect(localStorage.getItem(localStorageKey)).toBe(token); | |
}); | |
it('should return error detail, prepended with custom message', async function () { | |
const { getByText, findByTestId, mockedAxios } = setup(); | |
mockedAxios.get.mockRejectedValueOnce({ | |
response: { | |
status: 400, | |
statusText: 'Bad Request', | |
data: { | |
detail: 'Unable to process request', | |
errors: ['Invalid request'], | |
}, | |
}, | |
} as AxiosError); | |
act(() => { | |
userEvent.click(getByText(/Send Request/i)); | |
}); | |
const message = await findByTestId('error-message'); | |
const type = await findByTestId('error-type'); | |
expect(message.innerHTML).toBe('Oops! Unable to process request'); | |
expect(type.innerHTML).toBe('error'); | |
expect(localStorage.getItem(localStorageKey)).toBe(token); | |
}); | |
it('should return concatenated errors messages if error detail is absent, prepended with custom message', async function () { | |
const { getByText, findByTestId, mockedAxios } = setup(); | |
mockedAxios.get.mockRejectedValueOnce({ | |
response: { | |
status: 400, | |
statusText: 'Bad Request', | |
data: { | |
errors: ['Invalid request', 'Invalid Id'], | |
}, | |
}, | |
} as AxiosError); | |
act(() => { | |
userEvent.click(getByText(/Send Request/i)); | |
}); | |
const message = await findByTestId('error-message'); | |
const type = await findByTestId('error-type'); | |
expect(message.innerHTML).toBe('Oops! Invalid request, Invalid Id'); | |
expect(type.innerHTML).toBe('error'); | |
expect(localStorage.getItem(localStorageKey)).toBe(token); | |
}); | |
it('should return serialised data object if error detail and errors are absent', async function () { | |
const { getByText, findByTestId, mockedAxios } = setup(); | |
mockedAxios.get.mockRejectedValueOnce({ | |
response: { | |
status: 400, | |
statusText: 'Bad Request', | |
data: { | |
message: 'Invalid request', | |
}, | |
}, | |
} as AxiosError); | |
act(() => { | |
userEvent.click(getByText(/Send Request/i)); | |
}); | |
const message = await findByTestId('error-message'); | |
const type = await findByTestId('error-type'); | |
expect(message.innerHTML).toMatchInlineSnapshot( | |
`"Oops! {\\"message\\":\\"Invalid request\\"}"` | |
); | |
expect(type.innerHTML).toBe('error'); | |
expect(localStorage.getItem(localStorageKey)).toBe(token); | |
}); | |
it('should alert server connection failed if no response was received', async function () { | |
const { getByText, findByTestId, mockedAxios } = setup(); | |
mockedAxios.get.mockRejectedValueOnce({ | |
request: {}, | |
} as AxiosError); | |
act(() => { | |
userEvent.click(getByText(/Send Request/i)); | |
}); | |
const message = await findByTestId('error-message'); | |
const type = await findByTestId('error-type'); | |
expect(message.innerHTML).toMatchInlineSnapshot(`"Server connection failed"`); | |
expect(type.innerHTML).toBe('error'); | |
expect(localStorage.getItem(localStorageKey)).toBe(token); | |
}); | |
it(`if these is no response/request properties in the returned error, in development mode, | |
1. should alert message on error object, | |
2. should log error json. | |
3. should print error in the console`, async function () { | |
jest.spyOn(console, 'error').mockImplementation(() => {}); | |
jest.spyOn(console, 'log').mockImplementation(() => {}); | |
environment.production = false; | |
const { getByText, findByTestId, mockedAxios } = setup(); | |
mockedAxios.get.mockRejectedValueOnce({ | |
message: 'Invalid request', | |
} as AxiosError); | |
act(() => { | |
userEvent.click(getByText(/Send Request/i)); | |
}); | |
const message = await findByTestId('error-message'); | |
const type = await findByTestId('error-type'); | |
expect(message.innerHTML).toMatchInlineSnapshot(`"Invalid request"`); | |
expect(type.innerHTML).toBe('error'); | |
expect(localStorage.getItem(localStorageKey)).toBe(token); | |
expect(console.log).toHaveBeenCalledWith(`{"message":"Invalid request"}`); | |
expect(console.error).toHaveBeenCalledWith({ | |
message: 'Invalid request', | |
}); | |
jest.restoreAllMocks(); | |
}); | |
it(`if these is no response/request properties in the returned error, in production mode, | |
1. should not alert message on error object, | |
2. should not log error json. | |
3. should print error in the console`, async function () { | |
jest.spyOn(console, 'error').mockImplementation(() => {}); | |
environment.production = true; | |
const { getByText, mockedAxios } = setup(); | |
mockedAxios.get.mockRejectedValueOnce({ | |
message: 'Invalid request', | |
} as AxiosError); | |
act(() => { | |
userEvent.click(getByText(/Send Request/i)); | |
}); | |
await waitFor(() => { | |
expect(console.error).toHaveBeenCalledWith({ | |
message: 'Invalid request', | |
}); | |
}); | |
jest.restoreAllMocks(); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AxiosError, AxiosResponse } from 'axios'; | |
import { environment } from '@epic/environments'; | |
import { useCallback } from 'react'; | |
import { useMessage } from '../message/MessageContext'; | |
import { concatValues } from '../utils/concatValues'; | |
import { localStorageKey } from './useClient'; | |
type Translator = (english?: string) => string | undefined; | |
function extractErrorMessage(response: AxiosResponse): string { | |
if (response.data.detail) { | |
return response.data.detail; | |
} | |
if (response.data && response.data.errors) { | |
return concatValues(response.data.errors); | |
} | |
return JSON.stringify(response.data); | |
} | |
export function useErrorHandler({ | |
message, | |
t = (english?: string) => english, | |
}: { | |
message?: string; | |
t?: Translator; | |
} = {}) { | |
const { sendError } = useMessage(); | |
const processResponseError = useCallback( | |
({ response, message, t }: { response: AxiosResponse; message?: string; t: Translator }) => { | |
if (response?.status === 401) { | |
localStorage.removeItem(localStorageKey); | |
sendError(t('Login failed')); | |
window.location.assign(window.location.href); | |
return; | |
} | |
if (response?.status === 403) { | |
sendError( | |
t('Authorisation failed. Please sign in with an account with the required permissions.') | |
); | |
return; | |
} | |
sendError(`${message} ${t(extractErrorMessage(response))}`.trim()); | |
return; | |
}, | |
[sendError] | |
); | |
const errorHandler = useCallback( | |
(error: AxiosError<any>) => { | |
if (error.response) { | |
// The request was made and the server responded with a status code | |
// that falls out of the range of 2xx | |
return processResponseError({ response: error.response, message, t }); | |
} | |
if (error.request) { | |
// The request was made but no response was received | |
// `error.request` | |
// is an instance of XMLHttpRequest in the browser and an instance of | |
// http.ClientRequest in node.js | |
sendError(t(`Server connection failed`)); | |
return; | |
} | |
if (!environment.production) { | |
// Something happened in setting up the request that triggered an Error | |
sendError(t(error.message)); | |
console.log(JSON.stringify(error)); | |
} | |
console.error(error); | |
}, | |
[message, processResponseError, sendError, t] | |
); | |
return { errorHandler }; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment