Skip to content
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
40 changes: 40 additions & 0 deletions packages/functional-tests/tests/misc/errorViews.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { expect, test } from '../../lib/fixtures/standard';

test.describe('error views', () => {
test('404 view links back to signin', async ({
page,
pages: { signin, fourOhFour },
}) => {
await fourOhFour.goto('load');
await expect(fourOhFour.header).toBeVisible();
await expect(fourOhFour.homeLink).toBeVisible();
await fourOhFour.homeLink.click();
await page.waitForLoadState();
await expect(signin.emailFirstHeading).toBeVisible();
});

test('app error view renders for an invalid query parameter', async ({
target,
page,
pages: { signin },
}) => {
// A malformed redirect_to fails query-parameter validation, which the app
// surfaces through the redesigned AppErrorDialog instead of the signin form.
const response = await page.goto(
`${target.contentServerUrl}/?redirect_to=javascript:alert(1)`
);

// The WAF may block the request (Fastly returns 406) before it reaches the
// app; that already prevents the bad parameter, so there is no view to check.
if (response && response.status() === 406) {
return;
}

await expect(signin.badRequestHeading).toBeVisible();
await expect(signin.emailFirstHeading).toBeHidden();
});
});
19 changes: 0 additions & 19 deletions packages/functional-tests/tests/misc/fourOhFour.spec.ts

This file was deleted.

17 changes: 13 additions & 4 deletions packages/fxa-react/components/AppErrorBoundary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type AppErrorBoundaryProps = {

type AppErrorBoundaryState = {
error: Error | undefined;
sentryEventId: string | undefined;
};

class AppErrorBoundary extends React.Component<
Expand All @@ -20,11 +21,12 @@ class AppErrorBoundary extends React.Component<
> {
state: {
error: undefined | Error;
sentryEventId: undefined | string;
};

constructor(props: AppErrorBoundaryProps) {
super(props);
this.state = { error: undefined };
this.state = { error: undefined, sentryEventId: undefined };
}

static getDerivedStateFromError(error: Error) {
Expand All @@ -33,12 +35,19 @@ class AppErrorBoundary extends React.Component<

componentDidCatch(error: Error) {
console.error('AppError', error);
Sentry.captureException(error, { tags: { source: 'AppErrorBoundary' } });
const sentryEventId = Sentry.captureException(error, {
tags: { source: 'AppErrorBoundary' },
});
this.setState({ sentryEventId });
}

render() {
const { error } = this.state;
return error ? <AppErrorDialog /> : this.props.children;
const { error, sentryEventId } = this.state;
return error ? (
<AppErrorDialog {...{ error, sentryEventId }} />
) : (
this.props.children
);
}
}

Expand Down
8 changes: 6 additions & 2 deletions packages/fxa-react/components/AppErrorDialog/en.ftl
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
## FxA React - Strings shared between multiple FxA products for application error dialog

app-general-err-heading = General application error
app-general-err-message = Something went wrong. Please try again later.
app-something-went-wrong-heading = Something went wrong
app-something-went-wrong-message = We’ve been notified of the issue. Refresh the page to try again.
# $errorId (String) - Unique identifier for the error report, used to look it up in our monitoring system
app-error-id = Error ID: { $errorId }
# Expandable toggle that reveals technical details about the error
app-error-details-summary = Error details
Comment on lines +3 to +8

# Specific handling for issues when bad or missing query parameters are detected
app-query-parameter-err-heading = Bad Request: Invalid Query Parameters
71 changes: 67 additions & 4 deletions packages/fxa-react/components/AppErrorDialog/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,78 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import React from 'react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AppErrorDialog from '.';
import { renderWithLocalizationProvider } from '../../lib/test-utils/localizationProvider';

const MOCK_SENTRY_EVENT_ID = '0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d';

describe('AppErrorDialog', () => {
it('renders a general error dialog', () => {
const { queryByTestId } = renderWithLocalizationProvider(
<AppErrorDialog />
it('renders the "Something went wrong" heading for the general error type', () => {
renderWithLocalizationProvider(<AppErrorDialog />);

expect(
screen.getByRole('heading', { name: 'Something went wrong' })
).toBeInTheDocument();
});

it('renders the invalid query parameters heading for the query-parameter-violation error type', () => {
renderWithLocalizationProvider(
<AppErrorDialog errorType="query-parameter-violation" />
);

expect(
screen.getByRole('heading', {
name: 'Bad Request: Invalid Query Parameters',
})
).toBeInTheDocument();
});

it('omits the "we\'ve been notified" message for the query-parameter-violation type', () => {
renderWithLocalizationProvider(
<AppErrorDialog errorType="query-parameter-violation" />
);

expect(
screen.queryByText(/we’ve been notified of the issue/i)
).not.toBeInTheDocument();
});

it('shows the Sentry error ID when provided', () => {
renderWithLocalizationProvider(
<AppErrorDialog sentryEventId={MOCK_SENTRY_EVENT_ID} />
);

expect(
screen.getByText(`Error ID: ${MOCK_SENTRY_EVENT_ID}`)
).toBeInTheDocument();
});

it('reveals the error message when the error details toggle is expanded', async () => {
const user = userEvent.setup();
renderWithLocalizationProvider(
<AppErrorDialog error={new Error('Invalid redirect parameter')} />
);

expect(queryByTestId('error-loading-app')).toBeInTheDocument();
expect(screen.getByText('Invalid redirect parameter')).not.toBeVisible();

await user.click(screen.getByText('Error details'));

expect(screen.getByText('Invalid redirect parameter')).toBeVisible();
});
Comment on lines +54 to 65

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repo resolves jest-environment-jsdom@29.7.0 (not 27 / jsdom 16), and the <details>/toBeVisible() assertions pass both in isolation and in the suite. The flakiness premise doesn't apply here, so leaving the test as-is.


it('includes errno and code in the details when the error provides them', async () => {
const user = userEvent.setup();
const error = Object.assign(new Error('Invalid redirect parameter'), {
errno: 102,
code: 400,
});
renderWithLocalizationProvider(<AppErrorDialog error={error} />);

await user.click(screen.getByText('Error details'));

expect(screen.getByText(/errno: 102/)).toBeVisible();
expect(screen.getByText(/code: 400/)).toBeVisible();
});
});
74 changes: 56 additions & 18 deletions packages/fxa-react/components/AppErrorDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,45 @@ import { Localized } from '@fluent/react';

export type ErrorType = 'general' | 'query-parameter-violation';

const AppErrorDialog = ({ errorType }: { errorType?: ErrorType }) => {
if (errorType == null) {
errorType = 'general';
}
type AppErrorDialogProps = {
errorType?: ErrorType;
/** The caught error; its message shows under the details toggle. */
error?: Error;
/** Sentry event ID from captureException; search Sentry for id:<eventId>. */
sentryEventId?: string;
};

const AppErrorDialog = ({
errorType = 'general',
error,
sentryEventId,
}: AppErrorDialogProps) => {
// Auth UI errors carry errno/code; include them since some share a message.
const { errno, code } = (error ?? {}) as { errno?: number; code?: number };
const errorDetails = [
error?.message,
errno != null && `errno: ${errno}`,
code != null && `code: ${code}`,
]
.filter(Boolean)
.join('\n');

return (
<div className="bg-grey-20 flex items-center flex-col justify-center">
<div className="text-center max-w-lg">
{errorType === 'general' && (
<Localized id="app-general-err-heading">
<main className="flex min-h-screen flex-col items-center justify-center bg-grey-20 dark:bg-grey-900">
<section className="card text-center">
{errorType === 'general' ? (
<Localized id="app-something-went-wrong-heading">
<h2
className="text-grey-900 font-header text-lg font-bold mb-3"
className="card-header text-grey-900 dark:text-grey-100"
data-testid="error-loading-app"
>
General application error
Something went wrong
</h2>
</Localized>
)}
{errorType === 'query-parameter-violation' && (
) : (
<Localized id="app-query-parameter-err-heading">
<h2
className="text-grey-900 font-header text-lg font-bold mb-3"
className="card-header text-grey-900 dark:text-grey-100"
data-testid="error-bad-query-parameters"
>
Bad Request: Invalid Query Parameters
Expand All @@ -37,14 +54,35 @@ const AppErrorDialog = ({ errorType }: { errorType?: ErrorType }) => {
)}

{errorType === 'general' && (
<Localized id="app-general-err-message">
<p className="text-grey-400">
Something went wrong. Please try again later.
<Localized id="app-something-went-wrong-message">
<p className="mt-3 text-sm text-grey-500 dark:text-grey-200">
We’ve been notified of the issue. Refresh the page to try again.
</p>
</Localized>
)}

{sentryEventId && (
<Localized id="app-error-id" vars={{ errorId: sentryEventId }}>
<p className="mt-6 text-xs text-grey-400 dark:text-grey-300">
{`Error ID: ${sentryEventId}`}
</p>
</Localized>
)}
</div>
</div>

{errorDetails && (
<details className="mt-2">
<Localized id="app-error-details-summary">
<summary className="cursor-pointer text-xs text-grey-400 dark:text-grey-300">
Error details
</summary>
</Localized>
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words rounded bg-grey-10 dark:bg-grey-800 p-3 text-start text-xs text-grey-500 dark:text-grey-200">
{errorDetails}
</pre>
</details>
)}
</section>
</main>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import React from 'react';
import { screen } from '@testing-library/react';
import * as Sentry from '@sentry/browser';
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
import { AppErrorBoundary } from '.';
import { ModelValidationErrors } from '../../lib/model-data';

jest.mock('@sentry/browser', () => ({
captureException: jest.fn(),
}));

const MOCK_SENTRY_EVENT_ID = '0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d';

// Error boundaries log the thrown error; keep test output clean.
window.console.error = jest.fn();

const BadComponent = ({ error }: { error: Error }) => {
throw error;
};

describe('AppErrorBoundary', () => {
beforeEach(() => {
jest.clearAllMocks();
(Sentry.captureException as jest.Mock).mockReturnValue(
MOCK_SENTRY_EVENT_ID
);
});

it('renders children when no error occurs', () => {
renderWithLocalizationProvider(
<AppErrorBoundary>
<p>all good</p>
</AppErrorBoundary>
);

expect(screen.getByText('all good')).toBeInTheDocument();
});

it('catches a thrown error and displays the Sentry event ID', () => {
renderWithLocalizationProvider(
<AppErrorBoundary>
<BadComponent error={new Error('boom')} />
</AppErrorBoundary>
);

expect(
screen.getByRole('heading', { name: 'Something went wrong' })
).toBeInTheDocument();
expect(
screen.getByText(`Error ID: ${MOCK_SENTRY_EVENT_ID}`)
).toBeInTheDocument();
});

it('renders the invalid query parameters heading for ModelValidationErrors', () => {
renderWithLocalizationProvider(
<AppErrorBoundary>
<BadComponent
error={new ModelValidationErrors([], {}, 'QueryParameterValidation')}
/>
</AppErrorBoundary>
);

expect(
screen.getByRole('heading', {
name: 'Bad Request: Invalid Query Parameters',
})
).toBeInTheDocument();
});
});
Loading
Loading