Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

show special error state for ChunkLoadError #2834

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions frontend/src/components/error/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { Button, Split, SplitItem, Title } from '@patternfly/react-core';
import { TimesIcon } from '@patternfly/react-icons';
import ErrorDetails from './ErrorDetails';
import UpdateState from './UpdateState';

type ErrorBoundaryProps = {
children?: React.ReactNode;
Expand All @@ -13,6 +14,7 @@ type ErrorBoundaryState =
hasError: true;
error: Error;
errorInfo: React.ErrorInfo;
isUpdateState: boolean;
};

class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
Expand All @@ -28,6 +30,7 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
hasError: true,
error,
errorInfo,
isUpdateState: error.name === 'ChunkLoadError',
});
// eslint-disable-next-line no-console
console.error('Caught error:', error, errorInfo);
Expand All @@ -38,17 +41,37 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
const { hasError } = this.state;

if (hasError) {
const { error, errorInfo } = this.state;
const { error, errorInfo, isUpdateState } = this.state;
if (isUpdateState) {
return (
<UpdateState
onClose={() => this.setState((prevState) => ({ ...prevState, isUpdateState: false }))}
/>
);
}
return (
<div className="pf-v5-u-p-lg">
<div className="pf-v5-u-p-lg" data-testid="error-boundary">
<Split>
<SplitItem isFilled>
<Title headingLevel="h1" className="pf-v5-u-mb-lg">
An error occurred.
<Title headingLevel="h1" className="pf-v5-u-mb-sm">
An error occurred
</Title>
<p className="pf-v5-u-mb-md">
Try{' '}
<Button
data-testid="reload-link"
variant="link"
isInline
onClick={() => window.location.reload()}
>
reloading
</Button>{' '}
the page if there was a recent update.
</p>
</SplitItem>
<SplitItem>
<Button
data-testid="close-error-button"
variant="plain"
aria-label="Close"
onClick={() => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/error/ErrorDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const ErrorDetails: React.FC<ErrorDetailsProps> = ({
stack,
}) => (
<>
<Title headingLevel="h2" className="pf-v5-u-mb-lg">
<Title headingLevel="h2" className="pf-v5-u-mb-md">
{title}
</Title>
<DescriptionList>
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/components/error/UpdateState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from 'react';
import {
Button,
EmptyState,
EmptyStateActions,
EmptyStateBody,
EmptyStateFooter,
EmptyStateHeader,
EmptyStateIcon,
EmptyStateVariant,
PageSection,
PageSectionVariants,
} from '@patternfly/react-core';
import { PathMissingIcon } from '@patternfly/react-icons';

type Props = {
onClose: () => void;
};

const UpdateState: React.FC<Props> = ({ onClose }) => (
<PageSection variant={PageSectionVariants.light} data-testid="error-update-state">
<EmptyState variant={EmptyStateVariant.full}>
<EmptyStateHeader
titleText="An error occurred"
icon={<EmptyStateIcon icon={PathMissingIcon} />}
headingLevel="h2"
/>
<EmptyStateBody>This is likely the result of a recent update.</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
<Button
variant="primary"
onClick={() => window.location.reload()}
data-testid="reload-button"
>
Reload this page
</Button>
</EmptyStateActions>
<EmptyStateActions>
<Button variant="link" onClick={onClose} data-testid="show-error-button">
Show error
</Button>
</EmptyStateActions>
</EmptyStateFooter>
</EmptyState>
</PageSection>
);

export default UpdateState;
74 changes: 74 additions & 0 deletions frontend/src/components/error/__tests__/ErrorBoundary.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as React from 'react';
import '@testing-library/jest-dom';
import { act, render, screen } from '@testing-library/react';
import ErrorBoundary from '~/components/error/ErrorBoundary';

class ChunkLoadError extends Error {
name = 'ChunkLoadError';
}

describe('ErrorBoundary', () => {
it('should handle regular error', async () => {
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: jest.fn() },
});

let showError = true;
const TestComponent = () => {
if (showError) {
throw new Error('regular error');
}
return <div data-testid="testing-123">testing</div>;
};
render(
<ErrorBoundary>
<TestComponent />
</ErrorBoundary>,
);
await screen.findByTestId('error-boundary');
await screen.findByText('regular error');

// reload link
const reloadButton = await screen.findByTestId('reload-link');
act(() => reloadButton.click());
expect(window.location.reload).toHaveBeenCalled();

// close error
showError = false;
const closeButton = await screen.findByTestId('close-error-button');
act(() => closeButton.click());
expect(await screen.queryByTestId('error-boundary')).not.toBeInTheDocument();
await screen.findByTestId('testing-123');
});

it('should handle ChunkLoadError', async () => {
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: jest.fn() },
});

const ErrorComponent = () => {
throw new ChunkLoadError('a chunk load error');
};
render(
<ErrorBoundary>
<ErrorComponent />
</ErrorBoundary>,
);
await screen.findByTestId('error-update-state');
expect(await screen.queryByTestId('error-boundary')).not.toBeInTheDocument();

// reload button
const reloadButton = await screen.findByTestId('reload-button');
act(() => reloadButton.click());
expect(window.location.reload).toHaveBeenCalled();

// show error
const showErrorButton = await screen.findByTestId('show-error-button');
act(() => showErrorButton.click());
expect(await screen.queryByTestId('error-update-state')).not.toBeInTheDocument();
await screen.findByTestId('error-boundary');
await screen.findByText('ChunkLoadError');
});
});
Loading