-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* upgraded deps * upgraded deps * initial error boundary * rename page * docs and translations * error page image * tests * pr fixes
- Loading branch information
Showing
20 changed files
with
560 additions
and
264 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,23 @@ | ||
.ls-error-page { | ||
&__container { | ||
max-width: 576px; | ||
} | ||
|
||
&__content { | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
} | ||
|
||
&__title { | ||
margin-bottom: 2rem; | ||
} | ||
|
||
&__button-row { | ||
margin-top: 4rem; | ||
|
||
ion-button { | ||
min-width: 12rem; | ||
} | ||
} | ||
} |
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,92 @@ | ||
import { IonButton, IonButtons, IonContent, IonFooter, IonPage, IonToolbar } from '@ionic/react'; | ||
import { FallbackProps } from 'react-error-boundary'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { ValidationError } from 'yup'; | ||
import { AxiosError } from 'axios'; | ||
|
||
import image from 'assets/img/face_surprise_melting.png'; | ||
import { PropsWithTestId } from '../types'; | ||
import './ErrorPage.scss'; | ||
import Header from '../Header/Header'; | ||
import Container from '../Content/Container'; | ||
import ButtonRow from '../Button/ButtonRow'; | ||
|
||
/** | ||
* Properties for the `ErrorPage` component. | ||
*/ | ||
interface ErrorPageProps extends FallbackProps, PropsWithTestId {} | ||
|
||
/** | ||
* The `ErrorPage` displays the attributes of an `Error`. | ||
* @param {ErrorPageProps} props - Component properties. | ||
* @returns {JSX.Element} JSX | ||
*/ | ||
const ErrorPage = ({ | ||
error, | ||
resetErrorBoundary, | ||
testid = 'page-error', | ||
}: ErrorPageProps): JSX.Element => { | ||
const { t } = useTranslation(); | ||
|
||
let title; | ||
let message; | ||
if (error instanceof ValidationError) { | ||
title = t('error-validation'); | ||
message = error.errors.reduce((msg, error) => `${msg} ${error}`); | ||
} else if (error instanceof AxiosError) { | ||
title = error.status ?? error.code; | ||
message = `${error.message}. ${error.config?.url}`; | ||
} else { | ||
title = error.name ?? t('error'); | ||
message = error.message ?? error; | ||
} | ||
|
||
return ( | ||
<IonPage className="ls-error-page" data-testid={testid}> | ||
<Header title={t('ionic-playground')} /> | ||
|
||
<IonContent className="ion-padding"> | ||
<Container fixed className="ls-error-page__container"> | ||
<div className="ls-error-page__content"> | ||
<img src={image} alt={title} /> | ||
|
||
<div | ||
className="text-3xl font-bold uppercase ls-error-page__title" | ||
data-testid={`${testid}-title`} | ||
> | ||
{title} | ||
</div> | ||
|
||
<div | ||
className="ion-text-center text-lg ls-error-page__message" | ||
data-testid={`${testid}-message`} | ||
> | ||
{message} | ||
</div> | ||
|
||
<ButtonRow className="ion-hide-md-down ls-error-page__button-row"> | ||
<IonButton | ||
color="medium" | ||
onClick={() => resetErrorBoundary()} | ||
data-testid={`${testid}-button`} | ||
> | ||
{t('label.try-again')} | ||
</IonButton> | ||
</ButtonRow> | ||
</div> | ||
</Container> | ||
</IonContent> | ||
<IonFooter className="ion-hide-md-up"> | ||
<IonToolbar> | ||
<IonButtons slot="end"> | ||
<IonButton onClick={() => resetErrorBoundary()} data-testid={`${testid}-footer-button`}> | ||
{t('label.try-again')} | ||
</IonButton> | ||
</IonButtons> | ||
</IonToolbar> | ||
</IonFooter> | ||
</IonPage> | ||
); | ||
}; | ||
|
||
export default ErrorPage; |
145 changes: 145 additions & 0 deletions
145
src/common/components/Error/__tests__/ErrorPage.test.tsx
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,145 @@ | ||
import { describe, expect, it, vi } from 'vitest'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { ValidationError } from 'yup'; | ||
import { AxiosError } from 'axios'; | ||
|
||
import { render, screen } from 'test/test-utils'; | ||
|
||
import ErrorPage from '../ErrorPage'; | ||
|
||
describe('ErrorPage', () => { | ||
it('should render successfully', async () => { | ||
// ARRANGE | ||
const error = new Error('error message'); | ||
const mockReset = vi.fn(); | ||
render(<ErrorPage error={error} resetErrorBoundary={mockReset} />); | ||
await screen.findByTestId('page-error'); | ||
|
||
// ASSERT | ||
expect(screen.getByTestId('page-error')).toBeDefined(); | ||
}); | ||
|
||
it('should display ValidationError', async () => { | ||
// ARRANGE | ||
const ve1 = new ValidationError('Required.'); | ||
const ve2 = new ValidationError('Max length is 100.'); | ||
const error = new ValidationError([ve1, ve2]); | ||
const mockReset = vi.fn(); | ||
render(<ErrorPage error={error} resetErrorBoundary={mockReset} />); | ||
await screen.findByTestId('page-error'); | ||
|
||
// ASSERT | ||
expect(screen.getByTestId('page-error')).toBeDefined(); | ||
expect(screen.getByTestId('page-error-title')).toHaveTextContent(/^Validation Error$/i); | ||
expect(screen.getByTestId('page-error-message')).toHaveTextContent( | ||
/^Required. Max length is 100.$/i, | ||
); | ||
}); | ||
|
||
it('should display AxiosError', async () => { | ||
// ARRANGE | ||
const config = { | ||
url: 'http://www.example.org/', | ||
}; | ||
// @ts-expect-error Only need partial object for test | ||
const error = new AxiosError('error message', AxiosError.ERR_BAD_REQUEST, config); | ||
const mockReset = vi.fn(); | ||
render(<ErrorPage error={error} resetErrorBoundary={mockReset} />); | ||
await screen.findByTestId('page-error'); | ||
|
||
// ASSERT | ||
expect(screen.getByTestId('page-error')).toBeDefined(); | ||
expect(screen.getByTestId('page-error-title')).toHaveTextContent(/^ERR_BAD_REQUEST$/i); | ||
expect(screen.getByTestId('page-error-message')).toHaveTextContent( | ||
/^error message. http:\/\/www.example.org\/$/i, | ||
); | ||
}); | ||
|
||
it('should display AxiosError with status code', async () => { | ||
// ARRANGE | ||
const config = { | ||
url: 'http://www.example.org/', | ||
}; | ||
// @ts-expect-error Only need partial object for test | ||
const error = new AxiosError('error message', AxiosError.ERR_BAD_REQUEST, config, config, { | ||
status: 404, | ||
}); | ||
const mockReset = vi.fn(); | ||
render(<ErrorPage error={error} resetErrorBoundary={mockReset} />); | ||
await screen.findByTestId('page-error'); | ||
|
||
// ASSERT | ||
expect(screen.getByTestId('page-error')).toBeDefined(); | ||
expect(screen.getByTestId('page-error-title')).toHaveTextContent(/^404$/i); | ||
expect(screen.getByTestId('page-error-message')).toHaveTextContent( | ||
/^error message. http:\/\/www.example.org\/$/i, | ||
); | ||
}); | ||
|
||
it('should display Error', async () => { | ||
// ARRANGE | ||
const error = new Error('error message'); | ||
const mockReset = vi.fn(); | ||
render(<ErrorPage error={error} resetErrorBoundary={mockReset} />); | ||
await screen.findByTestId('page-error'); | ||
|
||
// ASSERT | ||
expect(screen.getByTestId('page-error')).toBeDefined(); | ||
expect(screen.getByTestId('page-error-title')).toHaveTextContent(/^Error$/i); | ||
expect(screen.getByTestId('page-error-message')).toHaveTextContent(/^error message$/i); | ||
}); | ||
|
||
it('should display Error name', async () => { | ||
// ARRANGE | ||
const error = new Error('error message'); | ||
error.name = 'SpecificError'; | ||
const mockReset = vi.fn(); | ||
render(<ErrorPage error={error} resetErrorBoundary={mockReset} />); | ||
await screen.findByTestId('page-error'); | ||
|
||
// ASSERT | ||
expect(screen.getByTestId('page-error')).toBeDefined(); | ||
expect(screen.getByTestId('page-error-title')).toHaveTextContent(/^SpecificError$/i); | ||
expect(screen.getByTestId('page-error-message')).toHaveTextContent(/^error message$/i); | ||
}); | ||
|
||
it('should display plain error', async () => { | ||
// ARRANGE | ||
const mockReset = vi.fn(); | ||
render(<ErrorPage error={'error message'} resetErrorBoundary={mockReset} />); | ||
await screen.findByTestId('page-error'); | ||
|
||
// ASSERT | ||
expect(screen.getByTestId('page-error')).toBeDefined(); | ||
expect(screen.getByTestId('page-error-title')).toHaveTextContent(/^Error$/i); | ||
expect(screen.getByTestId('page-error-message')).toHaveTextContent(/^error message$/i); | ||
}); | ||
|
||
it('should attempt to reset clicking page body button', async () => { | ||
// ARRANGE | ||
const user = userEvent.setup(); | ||
const mockReset = vi.fn(); | ||
render(<ErrorPage error={'error message'} resetErrorBoundary={mockReset} />); | ||
await screen.findByTestId('page-error-button'); | ||
|
||
// ACT | ||
await user.click(screen.getByTestId('page-error-button')); | ||
|
||
// ASSERT | ||
expect(mockReset).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should attempt to reset clicking footer button', async () => { | ||
// ARRANGE | ||
const user = userEvent.setup(); | ||
const mockReset = vi.fn(); | ||
render(<ErrorPage error={'error message'} resetErrorBoundary={mockReset} />); | ||
await screen.findByTestId('page-error-footer-button'); | ||
|
||
// ACT | ||
await user.click(screen.getByTestId('page-error-footer-button')); | ||
|
||
// ASSERT | ||
expect(mockReset).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
Oops, something went wrong.