diff --git a/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.test.tsx b/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.test.tsx
new file mode 100644
index 0000000000..99db37d927
--- /dev/null
+++ b/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.test.tsx
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import { supersetTheme } from '@superset-ui/core';
+import BasicErrorAlert from './BasicErrorAlert';
+import { ErrorLevel } from './types';
+
+const mockedProps = {
+ body: 'Error body',
+ level: 'warning' as ErrorLevel,
+ title: 'Error title',
+};
+
+jest.mock('../Icon', () => ({
+ __esModule: true,
+ default: ({ name }: { name: string }) => (
+
+ ),
+}));
+
+test('should render', () => {
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
+
+test('should render warning icon', () => {
+ render();
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
+ expect(screen.getByTestId('icon')).toHaveAttribute(
+ 'data-name',
+ 'warning-solid',
+ );
+});
+
+test('should render error icon', () => {
+ const errorProps = {
+ ...mockedProps,
+ level: 'error' as ErrorLevel,
+ };
+ render();
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
+ expect(screen.getByTestId('icon')).toHaveAttribute(
+ 'data-name',
+ 'error-solid',
+ );
+});
+
+test('should render the error title', () => {
+ render();
+ expect(screen.getByText('Error title')).toBeInTheDocument();
+});
+
+test('should render the error body', () => {
+ render();
+ expect(screen.getByText('Error body')).toBeInTheDocument();
+});
+
+test('should render with warning theme', () => {
+ render();
+ expect(screen.getByRole('alert')).toHaveStyle(
+ `
+ backgroundColor: ${supersetTheme.colors.warning.light2};
+ `,
+ );
+});
+
+test('should render with error theme', () => {
+ const errorProps = {
+ ...mockedProps,
+ level: 'error' as ErrorLevel,
+ };
+ render();
+ expect(screen.getByRole('alert')).toHaveStyle(
+ `
+ backgroundColor: ${supersetTheme.colors.error.light2};
+ `,
+ );
+});
diff --git a/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx b/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx
index b66bebf3c2..6c460096ad 100644
--- a/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx
+++ b/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx
@@ -55,7 +55,7 @@ export default function BasicErrorAlert({
title,
}: BasicErrorAlertProps) {
return (
-
+
{
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
+
+test('should render the error message', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText('Error message')).toBeInTheDocument();
+});
+
+test('should render the issue codes', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText(/This may be triggered by:/)).toBeInTheDocument();
+ expect(screen.getByText(/Issue code message A/)).toBeInTheDocument();
+ expect(screen.getByText(/Issue code message B/)).toBeInTheDocument();
+});
+
+test('should render the engine name', () => {
+ render();
+ expect(screen.getByText(/Engine name/)).toBeInTheDocument();
+});
+
+test('should render the owners', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(
+ screen.getByText('Please reach out to the Chart Owners for assistance.'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('Chart Owners: Owner A, Owner B'),
+ ).toBeInTheDocument();
+});
+
+test('should NOT render the owners', () => {
+ const noVisualizationProps = {
+ ...mockedProps,
+ source: 'sqllab' as ErrorSource,
+ };
+ render(, {
+ useRedux: true,
+ });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(
+ screen.queryByText('Chart Owners: Owner A, Owner B'),
+ ).not.toBeInTheDocument();
+});
diff --git a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx
index c552d3a360..0454aca59e 100644
--- a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx
+++ b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx
@@ -46,7 +46,9 @@ function DatabaseErrorMessage({
{t('This may be triggered by:')}
{extra.issue_codes
- .map(issueCode => )
+ .map(issueCode => (
+
+ ))
.reduce((prev, curr) => [prev,
, curr])}
{isVisualization && extra.owners && (
diff --git a/superset-frontend/src/components/ErrorMessage/ErrorAlert.test.tsx b/superset-frontend/src/components/ErrorMessage/ErrorAlert.test.tsx
new file mode 100644
index 0000000000..f9c54e9be7
--- /dev/null
+++ b/superset-frontend/src/components/ErrorMessage/ErrorAlert.test.tsx
@@ -0,0 +1,161 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import userEvent from '@testing-library/user-event';
+import { render, screen } from 'spec/helpers/testing-library';
+import { supersetTheme } from '@superset-ui/core';
+import ErrorAlert from './ErrorAlert';
+import { ErrorLevel, ErrorSource } from './types';
+
+const mockedProps = {
+ body: 'Error body',
+ level: 'warning' as ErrorLevel,
+ copyText: 'Copy text',
+ subtitle: 'Error subtitle',
+ title: 'Error title',
+ source: 'dashboard' as ErrorSource,
+};
+
+jest.mock('../Icon', () => ({
+ __esModule: true,
+ default: ({ name }: { name: string }) => (
+
+ ),
+}));
+
+test('should render', () => {
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
+
+test('should render warning icon', () => {
+ render();
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
+ expect(screen.getByTestId('icon')).toHaveAttribute(
+ 'data-name',
+ 'warning-solid',
+ );
+});
+
+test('should render error icon', () => {
+ const errorProps = {
+ ...mockedProps,
+ level: 'error' as ErrorLevel,
+ };
+ render();
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
+ expect(screen.getByTestId('icon')).toHaveAttribute(
+ 'data-name',
+ 'error-solid',
+ );
+});
+
+test('should render the error title', () => {
+ const titleProps = {
+ ...mockedProps,
+ source: 'explore' as ErrorSource,
+ };
+ render();
+ expect(screen.getByText('Error title')).toBeInTheDocument();
+});
+
+test('should render the error subtitle', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText('Error subtitle')).toBeInTheDocument();
+});
+
+test('should render the error body', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText('Error body')).toBeInTheDocument();
+});
+
+test('should render the See more button', () => {
+ const seemoreProps = {
+ ...mockedProps,
+ source: 'explore' as ErrorSource,
+ };
+ render();
+ expect(screen.getByRole('button')).toBeInTheDocument();
+ expect(screen.getByText('See more')).toBeInTheDocument();
+});
+
+test('should render the modal', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ expect(screen.getByText('Close')).toBeInTheDocument();
+});
+
+test('should NOT render the modal', () => {
+ const expandableProps = {
+ ...mockedProps,
+ source: 'explore' as ErrorSource,
+ };
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+});
+
+test('should render the See less button', () => {
+ const expandableProps = {
+ ...mockedProps,
+ source: 'explore' as ErrorSource,
+ };
+ render();
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText('See less')).toBeInTheDocument();
+ expect(screen.queryByText('See more')).not.toBeInTheDocument();
+});
+
+test('should render the Copy button', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText('Copy message')).toBeInTheDocument();
+});
+
+test('should render with warning theme', () => {
+ render();
+ expect(screen.getByRole('alert')).toHaveStyle(
+ `
+ backgroundColor: ${supersetTheme.colors.warning.light2};
+ `,
+ );
+});
+
+test('should render with error theme', () => {
+ const errorProps = {
+ ...mockedProps,
+ level: 'error' as ErrorLevel,
+ };
+ render();
+ expect(screen.getByRole('alert')).toHaveStyle(
+ `
+ backgroundColor: ${supersetTheme.colors.error.light2};
+ `,
+ );
+});
diff --git a/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx b/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx
index ce5ebe6926..d78f616d5b 100644
--- a/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx
+++ b/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx
@@ -103,7 +103,7 @@ export default function ErrorAlert({
const isExpandable = ['explore', 'sqllab'].includes(source);
return (
-
+
{
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
+
+test('should render the stacktrace', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText('Stacktrace')).toBeInTheDocument();
+});
+
+test('should render the link', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ const link = screen.getByRole('link');
+ expect(link).toHaveTextContent('(Request Access)');
+ expect(link).toHaveAttribute('href', mockedProps.link);
+});
diff --git a/superset-frontend/src/components/ErrorMessage/IssueCode.test.tsx b/superset-frontend/src/components/ErrorMessage/IssueCode.test.tsx
new file mode 100644
index 0000000000..e4be3bfa70
--- /dev/null
+++ b/superset-frontend/src/components/ErrorMessage/IssueCode.test.tsx
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import IssueCode from './IssueCode';
+
+const mockedProps = {
+ code: 1,
+ message: 'Error message',
+};
+
+test('should render', () => {
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
+
+test('should render the message', () => {
+ render();
+ expect(screen.getByText('Error message')).toBeInTheDocument();
+});
+
+test('should render the link', () => {
+ render();
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute(
+ 'href',
+ `https://superset.apache.org/docs/miscellaneous/issue-codes#issue-${mockedProps.code}`,
+ );
+});
diff --git a/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx
new file mode 100644
index 0000000000..d4664d53c6
--- /dev/null
+++ b/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx
@@ -0,0 +1,82 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import ParameterErrorMessage from './ParameterErrorMessage';
+import { ErrorLevel, ErrorSource, ErrorTypeEnum } from './types';
+
+const mockedProps = {
+ error: {
+ error_type: ErrorTypeEnum.MISSING_TEMPLATE_PARAMS_ERROR,
+ extra: {
+ template_parameters: { state: 'CA', country: 'ITA' },
+ undefined_parameters: ['stat', 'count'],
+ issue_codes: [
+ {
+ code: 1,
+ message: 'Issue code message A',
+ },
+ {
+ code: 2,
+ message: 'Issue code message B',
+ },
+ ],
+ },
+ level: 'error' as ErrorLevel,
+ message: 'Error message',
+ },
+ source: 'dashboard' as ErrorSource,
+};
+
+test('should render', () => {
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
+
+test('should render the default title', () => {
+ render();
+ expect(screen.getByText('Parameter error')).toBeInTheDocument();
+});
+
+test('should render the error message', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText('Error message')).toBeInTheDocument();
+});
+
+test('should render the issue codes', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText(/This may be triggered by:/)).toBeInTheDocument();
+ expect(screen.getByText(/Issue code message A/)).toBeInTheDocument();
+ expect(screen.getByText(/Issue code message B/)).toBeInTheDocument();
+});
+
+test('should render the suggestions', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText(/Did you mean:/)).toBeInTheDocument();
+ expect(screen.getByText('"state" instead of "stat?"')).toBeInTheDocument();
+ expect(screen.getByText('"country" instead of "count?"')).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.test.tsx
new file mode 100644
index 0000000000..e41308f538
--- /dev/null
+++ b/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.test.tsx
@@ -0,0 +1,104 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import TimeoutErrorMessage from './TimeoutErrorMessage';
+import { ErrorLevel, ErrorSource, ErrorTypeEnum } from './types';
+
+const mockedProps = {
+ error: {
+ error_type: ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR,
+ extra: {
+ issue_codes: [
+ {
+ code: 1,
+ message: 'Issue code message A',
+ },
+ {
+ code: 2,
+ message: 'Issue code message B',
+ },
+ ],
+ owners: ['Owner A', 'Owner B'],
+ timeout: 30,
+ },
+ level: 'error' as ErrorLevel,
+ message: 'Error message',
+ },
+ source: 'dashboard' as ErrorSource,
+};
+
+test('should render', () => {
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
+
+test('should render the default title', () => {
+ render();
+ expect(screen.getByText('Timeout error')).toBeInTheDocument();
+});
+
+test('should render the issue codes', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(screen.getByText(/This may be triggered by:/)).toBeInTheDocument();
+ expect(screen.getByText(/Issue code message A/)).toBeInTheDocument();
+ expect(screen.getByText(/Issue code message B/)).toBeInTheDocument();
+});
+
+test('should render the owners', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(
+ screen.getByText('Please reach out to the Chart Owners for assistance.'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('Chart Owners: Owner A, Owner B'),
+ ).toBeInTheDocument();
+});
+
+test('should NOT render the owners', () => {
+ const noVisualizationProps = {
+ ...mockedProps,
+ source: 'sqllab' as ErrorSource,
+ };
+ render(, {
+ useRedux: true,
+ });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(
+ screen.queryByText('Chart Owners: Owner A, Owner B'),
+ ).not.toBeInTheDocument();
+});
+
+test('should render the timeout message', () => {
+ render(, { useRedux: true });
+ const button = screen.getByText('See more');
+ userEvent.click(button);
+ expect(
+ screen.getByText(
+ /We’re having trouble loading this visualization. Queries are set to timeout after 30 seconds./,
+ ),
+ ).toBeInTheDocument();
+});
diff --git a/superset-frontend/spec/javascripts/components/ErrorMessage/getErrorMessageComponentRegistry_spec.tsx b/superset-frontend/src/components/ErrorMessage/getErrorMessageComponentRegistry.test.tsx
similarity index 59%
rename from superset-frontend/spec/javascripts/components/ErrorMessage/getErrorMessageComponentRegistry_spec.tsx
rename to superset-frontend/src/components/ErrorMessage/getErrorMessageComponentRegistry.test.tsx
index ebc278b6bd..6cff2de4d4 100644
--- a/superset-frontend/spec/javascripts/components/ErrorMessage/getErrorMessageComponentRegistry_spec.tsx
+++ b/superset-frontend/src/components/ErrorMessage/getErrorMessageComponentRegistry.test.tsx
@@ -30,37 +30,35 @@ const OVERRIDE_ERROR_MESSAGE_COMPONENT = (_: ErrorMessageComponentProps) => (
Custom error
);
-describe('getErrorMessageComponentRegistry', () => {
- it('returns undefined for a non existent key', () => {
- expect(getErrorMessageComponentRegistry().get('INVALID_KEY')).toEqual(
- undefined,
- );
- });
-
- it('returns a component for a set key', () => {
- getErrorMessageComponentRegistry().registerValue(
- 'VALID_KEY',
- ERROR_MESSAGE_COMPONENT,
- );
-
- expect(getErrorMessageComponentRegistry().get('VALID_KEY')).toEqual(
- ERROR_MESSAGE_COMPONENT,
- );
- });
-
- it('returns the correct component for an overridden key', () => {
- getErrorMessageComponentRegistry().registerValue(
- 'OVERRIDE_KEY',
- ERROR_MESSAGE_COMPONENT,
- );
-
- getErrorMessageComponentRegistry().registerValue(
- 'OVERRIDE_KEY',
- OVERRIDE_ERROR_MESSAGE_COMPONENT,
- );
-
- expect(getErrorMessageComponentRegistry().get('OVERRIDE_KEY')).toEqual(
- OVERRIDE_ERROR_MESSAGE_COMPONENT,
- );
- });
+test('should return undefined for a non existent key', () => {
+ expect(getErrorMessageComponentRegistry().get('INVALID_KEY')).toEqual(
+ undefined,
+ );
+});
+
+test('should return a component for a set key', () => {
+ getErrorMessageComponentRegistry().registerValue(
+ 'VALID_KEY',
+ ERROR_MESSAGE_COMPONENT,
+ );
+
+ expect(getErrorMessageComponentRegistry().get('VALID_KEY')).toEqual(
+ ERROR_MESSAGE_COMPONENT,
+ );
+});
+
+test('should return the correct component for an overridden key', () => {
+ getErrorMessageComponentRegistry().registerValue(
+ 'OVERRIDE_KEY',
+ ERROR_MESSAGE_COMPONENT,
+ );
+
+ getErrorMessageComponentRegistry().registerValue(
+ 'OVERRIDE_KEY',
+ OVERRIDE_ERROR_MESSAGE_COMPONENT,
+ );
+
+ expect(getErrorMessageComponentRegistry().get('OVERRIDE_KEY')).toEqual(
+ OVERRIDE_ERROR_MESSAGE_COMPONENT,
+ );
});