diff --git a/example-plugin-app/src/PluginOne.jsx b/example-plugin-app/src/PluginOne.jsx
index 35aa08701..658e7d585 100644
--- a/example-plugin-app/src/PluginOne.jsx
+++ b/example-plugin-app/src/PluginOne.jsx
@@ -1,10 +1,12 @@
/* eslint react/prop-types: off */
-import React from 'react';
+import React, { useCallback } from 'react';
import { Plugin } from '@edx/frontend-platform/plugins';
function Greeting({ subject }) {
- return
Hello {subject.toUpperCase()}
;
+ return (
+ Hello {subject.toUpperCase()}
+ )
}
function errorFallback(error) {
diff --git a/example/src/PluginsPage.jsx b/example/src/PluginsPage.jsx
index da1bcca7f..7ef612c2a 100644
--- a/example/src/PluginsPage.jsx
+++ b/example/src/PluginsPage.jsx
@@ -24,6 +24,7 @@ export default function PluginsPage() {
className="d-flex flex-column"
pluginProps={{
className: 'flex-grow-1',
+ title: 'example plugins',
}}
style={{
height: 400,
diff --git a/src/plugins/Plugin.jsx b/src/plugins/Plugin.jsx
index 76264a90a..82fdbcbf9 100644
--- a/src/plugins/Plugin.jsx
+++ b/src/plugins/Plugin.jsx
@@ -1,6 +1,8 @@
'use client';
-import React, { useEffect, useMemo, useState } from 'react';
+import React, {
+ useEffect, useMemo, useState,
+} from 'react';
import PropTypes from 'prop-types';
import { ErrorBoundary } from 'react-error-boundary';
import {
@@ -10,7 +12,7 @@ import { logError } from '../logging';
import { PLUGIN_RESIZE } from './data/constants';
// see example-plugin-app/src/PluginOne.jsx for example of customizing errorFallback
-function errorFallback() {
+function errorFallbackDefault() {
return (
@@ -33,6 +35,8 @@ export default function Plugin({
...style,
}), [dimensions, style]);
+ const errorFallback = errorFallbackProp || errorFallbackDefault;
+
// Error logging function
// Need to confirm: When an error is caught here, the logging will be sent to the child MFE's logging service
const logErrorToService = (error, info) => {
@@ -63,7 +67,7 @@ export default function Plugin({
return (
errorFallbackProp(error)}
+ FallbackComponent={errorFallback}
onError={logErrorToService}
>
{children}
@@ -82,7 +86,7 @@ Plugin.propTypes = {
Plugin.defaultProps = {
className: null,
- errorFallbackProp: errorFallback,
+ errorFallbackProp: null,
style: {},
ready: true,
};
diff --git a/src/plugins/Plugin.test.jsx b/src/plugins/Plugin.test.jsx
index bfef33277..709b85db6 100644
--- a/src/plugins/Plugin.test.jsx
+++ b/src/plugins/Plugin.test.jsx
@@ -1,9 +1,11 @@
+/* eslint react/prop-types: off */
+
import React from 'react';
-import { ErrorBoundary } from 'react-error-boundary';
import { render } from '@testing-library/react';
import { fireEvent } from '@testing-library/dom';
import '@testing-library/jest-dom';
+import { initializeMockApp } from '..';
import PluginContainer from './PluginContainer';
import Plugin from './Plugin';
import {
@@ -34,25 +36,21 @@ describe('PluginContainer', () => {
expect(container.firstChild).toBeNull();
});
- it('should render the desired fallback when the iframe fails to render', () => {
-
- });
-
- it('should render a PluginIFrame when given an iFrame config', async () => {
+ it('should render a Plugin iFrame Container when given an iFrame config', async () => {
const title = 'test plugin';
const component = (
- Fallback } />
+ Loading
} />
);
const { container } = render(component);
- const iframeElement = await container.querySelector('iframe');
+ const iframeElement = container.querySelector('iframe');
const fallbackElement = container.querySelector('div');
expect(iframeElement).toBeInTheDocument();
expect(fallbackElement).toBeInTheDocument();
- expect(fallbackElement.innerHTML).toEqual('Fallback');
+ expect(fallbackElement.innerHTML).toEqual('Loading');
// Ensure the iframe has the proper attributes
expect(iframeElement.attributes.getNamedItem('allow').value).toEqual(IFRAME_FEATURE_POLICY);
@@ -98,18 +96,88 @@ describe('PluginContainer', () => {
});
describe('Plugin', () => {
- const breakingArray = null;
- const failingMap = () => breakingArray.map(a => a);
- it('should render the desired fallback when the error boundary receives a React error', () => {
+ let logError = jest.fn();
+
+ beforeEach(async () => {
+ // This is a gross hack to suppress error logs in the invalid parentSelector test
+ jest.spyOn(console, 'error');
+ global.console.error.mockImplementation(() => {});
+
+ const { loggingService } = initializeMockApp();
+ logError = loggingService.logError;
+ });
+
+ afterEach(() => {
+ global.console.error.mockRestore();
+ jest.clearAllMocks();
+ });
+
+ const ExplodingComponent = () => {
+ throw new Error('booyah');
+ };
+
+ function HealthyComponent() {
+ return (
+ Hello World!
+ );
+ }
+
+ const errorFallback = () => (
+
+
+ Oh geez, this is not good at all.
+
+
+
+ );
+
+ it('should render children if no error', () => {
+ const component = (
+
+
+
+ );
+ const { container } = render(component);
+ expect(container).toHaveTextContent('Hello World!');
+ });
+
+ it('should throw an error if the child component fails', () => {
+ const component = (
+
+
+
+ );
+
+ render(component);
+
+ expect(logError).toHaveBeenCalledTimes(1);
+ expect(logError).toHaveBeenCalledWith(
+ new Error('booyah'),
+ expect.objectContaining({
+ stack: expect.stringContaining('ExplodingComponent'),
+ }),
+ );
+ });
+
+ it('should render the passed in fallback component when the error boundary receives a React error', () => {
+ const component = (
+
+
+
+ );
+
+ const { container } = render(component);
+ expect(container).toHaveTextContent('Oh geez');
+ });
+
+ it('should render the default fallback component when one is not passed into the Plugin', () => {
const component = (
- Something went wrong}>
-
- { failingMap }
-
-
+
+
+
);
const { container } = render(component);
- expect(container.firstChild).toHaveTextContent('Something went wrong');
+ expect(container).toHaveTextContent('Oops! An error occurred.');
});
});