-
-
Notifications
You must be signed in to change notification settings - Fork 10.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🌐 Updated Portal error handling to be i18n-friendly (#21176)
no ref - Portal was not set up in a way to allow for easy use of the i18n module for errors, as they weren't a React component - moved away from the class model to a functional component that could utilize React state (AppContext) I'm working on a different refactor that would convert more of Portal to hooks & functional components so that the codebase is more consistent and easier to read. This will have to work for the moment while that is being done, as that's no small task.
- Loading branch information
Showing
6 changed files
with
145 additions
and
36 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import React from 'react'; | ||
import {render} from '@testing-library/react'; | ||
import AppContext from '../../AppContext'; | ||
import {GetMessage, getErrorFromApiResponse, getMessageFromError} from '../../utils/errors'; | ||
|
||
// Mock AppContext | ||
jest.mock('../../AppContext', () => ({ | ||
__esModule: true, | ||
default: React.createContext({t: jest.fn()}) | ||
})); | ||
|
||
describe('GetMessage', () => { | ||
it('returns translated message when t function is available', () => { | ||
const tMock = jest.fn().mockImplementation(msg => `translated: ${msg}`); | ||
const {getByText} = render( | ||
<AppContext.Provider value={{t: tMock}}> | ||
<GetMessage message="Hello" /> | ||
</AppContext.Provider> | ||
); | ||
expect(getByText('translated: Hello')).toBeInTheDocument(); | ||
expect(tMock).toHaveBeenCalledWith('Hello'); | ||
}); | ||
|
||
it('returns original message when t function is not available', () => { | ||
const {getByText} = render( | ||
<AppContext.Provider value={{}}> | ||
<GetMessage message="Hello" /> | ||
</AppContext.Provider> | ||
); | ||
expect(getByText('Hello')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
describe('getErrorFromApiResponse', () => { | ||
// These tests are a little goofy because we're not testing the translation, we're testing that the function | ||
// returns an Error with the correct message. We're not testing the message itself because that's handled in the | ||
// getMessage function. And this doesn't run in react so the react component gets odd. | ||
it('returns Error with translated message for 400 status', async () => { | ||
const res = { | ||
status: 400, | ||
json: jest.fn().mockResolvedValue({errors: [{message: 'Bad Request'}]}) | ||
}; | ||
const error = await getErrorFromApiResponse(res); | ||
expect(error).toBeInstanceOf(Error); | ||
}); | ||
|
||
// These tests are a little goofy because we're not testing the translation, we're testing that the function | ||
// returns an Error with the correct message. We're not testing the message itself because that's handled in the | ||
// getMessage function. And this doesn't run in react so the react component gets odd. | ||
it('returns Error with translated message for 429 status', async () => { | ||
const res = { | ||
status: 429, | ||
json: jest.fn().mockResolvedValue({errors: [{message: 'Too Many Requests'}]}) | ||
}; | ||
const error = await getErrorFromApiResponse(res); | ||
expect(error).toBeInstanceOf(Error); | ||
}); | ||
|
||
it('returns undefined for other status codes', async () => { | ||
const res = {status: 200}; | ||
const error = await getErrorFromApiResponse(res); | ||
expect(error).toBeUndefined(); | ||
}); | ||
|
||
it('returns undefined when json parsing fails', async () => { | ||
const res = { | ||
status: 400, | ||
json: jest.fn().mockRejectedValue(new Error('JSON parse error')) | ||
}; | ||
const error = await getErrorFromApiResponse(res); | ||
expect(error).toBeUndefined(); | ||
}); | ||
}); | ||
|
||
describe('getMessageFromError', () => { | ||
it('returns GetMessage component with default message when provided', () => { | ||
const result = getMessageFromError(new Error('Test error'), 'Default message'); | ||
const {getByText} = render(<AppContext.Provider value={{}}>{result}</AppContext.Provider>); | ||
expect(getByText('Default message')).toBeInTheDocument(); | ||
}); | ||
|
||
it('returns GetMessage component with error message for Error instance', () => { | ||
const result = getMessageFromError(new Error('Test error')); | ||
const {getByText} = render(<AppContext.Provider value={{}}>{result}</AppContext.Provider>); | ||
expect(getByText('Test error')).toBeInTheDocument(); | ||
}); | ||
|
||
it('returns GetMessage component with error for non-Error objects', () => { | ||
const result = getMessageFromError('String error'); | ||
const {getByText} = render(<AppContext.Provider value={{}}>{result}</AppContext.Provider>); | ||
expect(getByText('String error')).toBeInTheDocument(); | ||
}); | ||
}); |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,48 @@ | ||
export class HumanReadableError extends Error { | ||
/** | ||
* Returns whether this response from the server is a human readable error and should be shown to the user. | ||
* @param {Response} res | ||
* @returns {HumanReadableError|undefined} | ||
*/ | ||
static async fromApiResponse(res) { | ||
// Bad request + Too many requests | ||
if (res.status === 400 || res.status === 429) { | ||
try { | ||
const json = await res.json(); | ||
if (json.errors && Array.isArray(json.errors) && json.errors.length > 0 && json.errors[0].message) { | ||
return new HumanReadableError(json.errors[0].message); | ||
} | ||
} catch (e) { | ||
// Failed to decode: ignore | ||
return false; | ||
import {useContext} from 'react'; | ||
import AppContext from '../AppContext'; | ||
|
||
/** | ||
* A component that gets the internationalized (i18n) message from the app context. | ||
* @param {{message: string}} props - The props object containing the message to be translated. | ||
* @returns {string} The translated message if AppContext.t is available, otherwise the original message. | ||
*/ | ||
export const GetMessage = ({message}) => { | ||
const {t} = useContext(AppContext); | ||
return t ? t(message) : message; | ||
}; | ||
|
||
/** | ||
* Creates a human-readable error from an API response. | ||
* @param {Response} res - The API response object. | ||
* @returns {Promise<Error|undefined>} A promise that resolves to an Error if one can be created, or undefined otherwise. | ||
*/ | ||
export async function getErrorFromApiResponse(res) { | ||
// Bad request + Too many requests | ||
if (res.status === 400 || res.status === 429) { | ||
try { | ||
const json = await res.json(); | ||
if (json.errors && Array.isArray(json.errors) && json.errors.length > 0 && json.errors[0].message) { | ||
return new Error(<GetMessage message={json.errors[0].message} />); | ||
} | ||
} catch (e) { | ||
// Failed to decode: ignore | ||
return undefined; | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
/** | ||
* Only output the message of an error if it is a human readable error and should be exposed to the user. | ||
* Otherwise it returns a default generic message. | ||
*/ | ||
static getMessageFromError(error, defaultMessage) { | ||
if (error instanceof HumanReadableError) { | ||
return error.message; | ||
} | ||
return defaultMessage; | ||
/** | ||
* Creates a human-readable error message from an error object. | ||
* @param {Error} error - The error object. | ||
* @param {string} defaultMessage - The default message to use if a human-readable message can't be extracted. | ||
* @returns {React.ReactElement} A React element containing the human-readable error message. | ||
*/ | ||
export function getMessageFromError(error, defaultMessage) { | ||
if (defaultMessage) { | ||
return <GetMessage message={defaultMessage} />; | ||
} else if (error instanceof Error) { | ||
return <GetMessage message={error.message} />; | ||
} | ||
return <GetMessage message={error} />; | ||
} |