test: ErrorMessage components tests (#13358)

* Add tests for ErrorAlert

* Add BasicErrorAlert tests

* Add DatabaseErrorMessage tests

* Add useRedux property

* Finalize DatabaseErrorMessage tests

* Add ErrorMessageWithStackTrace tests

* Move getErrorMessageComponentRegistry test to dedicated directory

* Clean up getErrorMessageComponentRegistry

* Add IssueCode tests

* Add tests for ParameterErrorMessage

* Add tests for TimeoutErrorMessage

* Fix linting issue
diff --git a/superset-frontend/spec/javascripts/components/ErrorMessage/getErrorMessageComponentRegistry_spec.tsx b/superset-frontend/spec/javascripts/components/ErrorMessage/getErrorMessageComponentRegistry_spec.tsx
deleted file mode 100644
index ebc278b..0000000
--- a/superset-frontend/spec/javascripts/components/ErrorMessage/getErrorMessageComponentRegistry_spec.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * 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 getErrorMessageComponentRegistry from 'src/components/ErrorMessage/getErrorMessageComponentRegistry';
-import { ErrorMessageComponentProps } from 'src/components/ErrorMessage/types';
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const ERROR_MESSAGE_COMPONENT = (_: ErrorMessageComponentProps) => (
-  <div>Test error</div>
-);
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const OVERRIDE_ERROR_MESSAGE_COMPONENT = (_: ErrorMessageComponentProps) => (
-  <div>Custom error</div>
-);
-
-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,
-    );
-  });
-});
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 0000000..99db37d
--- /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 }) => (
+    <div data-test="icon" data-name={name} />
+  ),
+}));
+
+test('should render', () => {
+  const { container } = render(<BasicErrorAlert {...mockedProps} />);
+  expect(container).toBeInTheDocument();
+});
+
+test('should render warning icon', () => {
+  render(<BasicErrorAlert {...mockedProps} />);
+  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(<BasicErrorAlert {...errorProps} />);
+  expect(screen.getByTestId('icon')).toBeInTheDocument();
+  expect(screen.getByTestId('icon')).toHaveAttribute(
+    'data-name',
+    'error-solid',
+  );
+});
+
+test('should render the error title', () => {
+  render(<BasicErrorAlert {...mockedProps} />);
+  expect(screen.getByText('Error title')).toBeInTheDocument();
+});
+
+test('should render the error body', () => {
+  render(<BasicErrorAlert {...mockedProps} />);
+  expect(screen.getByText('Error body')).toBeInTheDocument();
+});
+
+test('should render with warning theme', () => {
+  render(<BasicErrorAlert {...mockedProps} />);
+  expect(screen.getByRole('alert')).toHaveStyle(
+    `
+      backgroundColor: ${supersetTheme.colors.warning.light2};
+    `,
+  );
+});
+
+test('should render with error theme', () => {
+  const errorProps = {
+    ...mockedProps,
+    level: 'error' as ErrorLevel,
+  };
+  render(<BasicErrorAlert {...errorProps} />);
+  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 b66bebf..6c46009 100644
--- a/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx
+++ b/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx
@@ -55,7 +55,7 @@
   title,
 }: BasicErrorAlertProps) {
   return (
-    <StyledContainer level={level}>
+    <StyledContainer level={level} role="alert">
       <Icon
         name={level === 'error' ? 'error-solid' : 'warning-solid'}
         color={supersetTheme.colors[level].base}
diff --git a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx
new file mode 100644
index 0000000..0b23866
--- /dev/null
+++ b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx
@@ -0,0 +1,100 @@
+/**
+ * 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 userEvent from '@testing-library/user-event';
+import DatabaseErrorMessage from './DatabaseErrorMessage';
+import { ErrorLevel, ErrorSource, ErrorTypeEnum } from './types';
+
+const mockedProps = {
+  error: {
+    error_type: ErrorTypeEnum.DATABASE_SECURITY_ACCESS_ERROR,
+    extra: {
+      engine_name: 'Engine name',
+      issue_codes: [
+        {
+          code: 1,
+          message: 'Issue code message A',
+        },
+        {
+          code: 2,
+          message: 'Issue code message B',
+        },
+      ],
+      owners: ['Owner A', 'Owner B'],
+    },
+    level: 'error' as ErrorLevel,
+    message: 'Error message',
+  },
+  source: 'dashboard' as ErrorSource,
+};
+
+test('should render', () => {
+  const { container } = render(<DatabaseErrorMessage {...mockedProps} />);
+  expect(container).toBeInTheDocument();
+});
+
+test('should render the error message', () => {
+  render(<DatabaseErrorMessage {...mockedProps} />, { useRedux: true });
+  const button = screen.getByText('See more');
+  userEvent.click(button);
+  expect(screen.getByText('Error message')).toBeInTheDocument();
+});
+
+test('should render the issue codes', () => {
+  render(<DatabaseErrorMessage {...mockedProps} />, { 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(<DatabaseErrorMessage {...mockedProps} />);
+  expect(screen.getByText(/Engine name/)).toBeInTheDocument();
+});
+
+test('should render the owners', () => {
+  render(<DatabaseErrorMessage {...mockedProps} />, { 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(<DatabaseErrorMessage {...noVisualizationProps} />, {
+    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 c552d3a..0454aca 100644
--- a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx
+++ b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx
@@ -46,7 +46,9 @@
         {t('This may be triggered by:')}
         <br />
         {extra.issue_codes
-          .map<React.ReactNode>(issueCode => <IssueCode {...issueCode} />)
+          .map<React.ReactNode>(issueCode => (
+            <IssueCode {...issueCode} key={issueCode.code} />
+          ))
           .reduce((prev, curr) => [prev, <br />, curr])}
       </p>
       {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 0000000..f9c54e9
--- /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 }) => (
+    <div data-test="icon" data-name={name} />
+  ),
+}));
+
+test('should render', () => {
+  const { container } = render(<ErrorAlert {...mockedProps} />);
+  expect(container).toBeInTheDocument();
+});
+
+test('should render warning icon', () => {
+  render(<ErrorAlert {...mockedProps} />);
+  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(<ErrorAlert {...errorProps} />);
+  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(<ErrorAlert {...titleProps} />);
+  expect(screen.getByText('Error title')).toBeInTheDocument();
+});
+
+test('should render the error subtitle', () => {
+  render(<ErrorAlert {...mockedProps} />, { useRedux: true });
+  const button = screen.getByText('See more');
+  userEvent.click(button);
+  expect(screen.getByText('Error subtitle')).toBeInTheDocument();
+});
+
+test('should render the error body', () => {
+  render(<ErrorAlert {...mockedProps} />, { 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(<ErrorAlert {...seemoreProps} />);
+  expect(screen.getByRole('button')).toBeInTheDocument();
+  expect(screen.getByText('See more')).toBeInTheDocument();
+});
+
+test('should render the modal', () => {
+  render(<ErrorAlert {...mockedProps} />, { 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(<ErrorAlert {...expandableProps} />, { 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(<ErrorAlert {...expandableProps} />);
+  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(<ErrorAlert {...mockedProps} />, { useRedux: true });
+  const button = screen.getByText('See more');
+  userEvent.click(button);
+  expect(screen.getByText('Copy message')).toBeInTheDocument();
+});
+
+test('should render with warning theme', () => {
+  render(<ErrorAlert {...mockedProps} />);
+  expect(screen.getByRole('alert')).toHaveStyle(
+    `
+      backgroundColor: ${supersetTheme.colors.warning.light2};
+    `,
+  );
+});
+
+test('should render with error theme', () => {
+  const errorProps = {
+    ...mockedProps,
+    level: 'error' as ErrorLevel,
+  };
+  render(<ErrorAlert {...errorProps} />);
+  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 ce5ebe6..d78f616 100644
--- a/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx
+++ b/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx
@@ -103,7 +103,7 @@
   const isExpandable = ['explore', 'sqllab'].includes(source);
 
   return (
-    <ErrorAlertDiv level={level}>
+    <ErrorAlertDiv level={level} role="alert">
       <div className="top-row">
         <LeftSideContent>
           <Icon
diff --git a/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.test.tsx b/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.test.tsx
new file mode 100644
index 0000000..6e343eb
--- /dev/null
+++ b/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.test.tsx
@@ -0,0 +1,52 @@
+/**
+ * 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 userEvent from '@testing-library/user-event';
+import ErrorMessageWithStackTrace from './ErrorMessageWithStackTrace';
+import { ErrorLevel, ErrorSource } from './types';
+
+const mockedProps = {
+  level: 'warning' as ErrorLevel,
+  link: 'https://sample.com',
+  source: 'dashboard' as ErrorSource,
+  stackTrace: 'Stacktrace',
+};
+
+test('should render', () => {
+  const { container } = render(<ErrorMessageWithStackTrace {...mockedProps} />);
+  expect(container).toBeInTheDocument();
+});
+
+test('should render the stacktrace', () => {
+  render(<ErrorMessageWithStackTrace {...mockedProps} />, { useRedux: true });
+  const button = screen.getByText('See more');
+  userEvent.click(button);
+  expect(screen.getByText('Stacktrace')).toBeInTheDocument();
+});
+
+test('should render the link', () => {
+  render(<ErrorMessageWithStackTrace {...mockedProps} />, { 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 0000000..e4be3bf
--- /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(<IssueCode {...mockedProps} />);
+  expect(container).toBeInTheDocument();
+});
+
+test('should render the message', () => {
+  render(<IssueCode {...mockedProps} />);
+  expect(screen.getByText('Error message')).toBeInTheDocument();
+});
+
+test('should render the link', () => {
+  render(<IssueCode {...mockedProps} />);
+  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 0000000..d4664d5
--- /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(<ParameterErrorMessage {...mockedProps} />);
+  expect(container).toBeInTheDocument();
+});
+
+test('should render the default title', () => {
+  render(<ParameterErrorMessage {...mockedProps} />);
+  expect(screen.getByText('Parameter error')).toBeInTheDocument();
+});
+
+test('should render the error message', () => {
+  render(<ParameterErrorMessage {...mockedProps} />, { useRedux: true });
+  const button = screen.getByText('See more');
+  userEvent.click(button);
+  expect(screen.getByText('Error message')).toBeInTheDocument();
+});
+
+test('should render the issue codes', () => {
+  render(<ParameterErrorMessage {...mockedProps} />, { 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(<ParameterErrorMessage {...mockedProps} />, { 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 0000000..e41308f
--- /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(<TimeoutErrorMessage {...mockedProps} />);
+  expect(container).toBeInTheDocument();
+});
+
+test('should render the default title', () => {
+  render(<TimeoutErrorMessage {...mockedProps} />);
+  expect(screen.getByText('Timeout error')).toBeInTheDocument();
+});
+
+test('should render the issue codes', () => {
+  render(<TimeoutErrorMessage {...mockedProps} />, { 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(<TimeoutErrorMessage {...mockedProps} />, { 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(<TimeoutErrorMessage {...noVisualizationProps} />, {
+    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(<TimeoutErrorMessage {...mockedProps} />, { 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/src/components/ErrorMessage/getErrorMessageComponentRegistry.test.tsx b/superset-frontend/src/components/ErrorMessage/getErrorMessageComponentRegistry.test.tsx
new file mode 100644
index 0000000..6cff2de
--- /dev/null
+++ b/superset-frontend/src/components/ErrorMessage/getErrorMessageComponentRegistry.test.tsx
@@ -0,0 +1,64 @@
+/**
+ * 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 getErrorMessageComponentRegistry from 'src/components/ErrorMessage/getErrorMessageComponentRegistry';
+import { ErrorMessageComponentProps } from 'src/components/ErrorMessage/types';
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const ERROR_MESSAGE_COMPONENT = (_: ErrorMessageComponentProps) => (
+  <div>Test error</div>
+);
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const OVERRIDE_ERROR_MESSAGE_COMPONENT = (_: ErrorMessageComponentProps) => (
+  <div>Custom error</div>
+);
+
+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,
+  );
+});