diff --git a/example-plugin-app/src/PluginOne.jsx b/example-plugin-app/src/PluginOne.jsx
index 9737b1f08..3a461ee15 100644
--- a/example-plugin-app/src/PluginOne.jsx
+++ b/example-plugin-app/src/PluginOne.jsx
@@ -1,13 +1,14 @@
import React from 'react';
import { Plugin } from '@edx/frontend-platform/plugins';
+function Greeting({subject}) {
+ return
Hello {subject.toUpperCase()}
+}
+
export default function PluginOne() {
return (
-
-
- Site Maintenance
- The site will be going down for maintenance soon.
-
+
+
);
}
diff --git a/package-lock.json b/package-lock.json
index aacaa448e..84ed01af5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
"lodash.merge": "4.6.2",
"lodash.snakecase": "4.1.1",
"pubsub-js": "1.9.4",
+ "react-error-boundary": "^4.0.11",
"react-intl": "^5.25.0",
"universal-cookie": "4.0.4"
},
@@ -5729,6 +5730,22 @@
}
}
},
+ "node_modules/@testing-library/react-hooks/node_modules/react-error-boundary": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
+ "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -20056,17 +20073,12 @@
}
},
"node_modules/react-error-boundary": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
- "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
- "dev": true,
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz",
+ "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
- "engines": {
- "node": ">=10",
- "npm": ">=6"
- },
"peerDependencies": {
"react": ">=16.13.1"
}
diff --git a/package.json b/package.json
index 7b50e1786..946eb8869 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,7 @@
"lodash.merge": "4.6.2",
"lodash.snakecase": "4.1.1",
"pubsub-js": "1.9.4",
+ "react-error-boundary": "^4.0.11",
"react-intl": "^5.25.0",
"universal-cookie": "4.0.4"
},
diff --git a/src/plugins/Plugin.jsx b/src/plugins/Plugin.jsx
index 00b22237e..b194da2bc 100644
--- a/src/plugins/Plugin.jsx
+++ b/src/plugins/Plugin.jsx
@@ -1,10 +1,28 @@
+'use client';
+
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
+import { ErrorBoundary } from 'react-error-boundary';
import {
dispatchMountedEvent, dispatchReadyEvent, dispatchUnmountedEvent, useHostEvent,
} from './data/hooks';
import { PLUGIN_RESIZE } from './data/constants';
+function ErrorFallback(error) {
+ // we can customize the UI as we want
+ return (
+
+
+ Oops! An error occurred
+
+
+ {error.message}
+
+ {/* Additional custom error handling */}
+
+ );
+}
+
export default function Plugin({
children, className, style, ready,
}) {
@@ -13,6 +31,20 @@ export default function Plugin({
height: null,
});
+ const [errorMessage, setErrorMessage] = useState('');
+ const handleResetError = () => {
+ console.log('Error boundary reset');
+ setErrorMessage('');
+ // additional logic to perform code cleanup and state update actions
+ };
+
+ // Error logging function
+ function logErrorToService(error) {
+ // Use your preferred error logging service
+ setErrorMessage(error);
+ console.error('Caught an error:', errorMessage, error);
+ }
+
const finalStyle = useMemo(() => ({
...dimensions,
...style,
@@ -41,7 +73,13 @@ export default function Plugin({
return (
- {children}
+ logErrorToService()}
+ onReset={handleResetError}
+ >
+ {children}
+
);
}
diff --git a/src/plugins/Plugin.test.jsx b/src/plugins/Plugin.test.jsx
index b3e5a3904..65420b2c7 100644
--- a/src/plugins/Plugin.test.jsx
+++ b/src/plugins/Plugin.test.jsx
@@ -1,9 +1,11 @@
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 PluginContainer from './PluginContainer';
+import Plugin from './Plugin';
import {
IFRAME_PLUGIN, PLUGIN_MOUNTED, PLUGIN_READY, PLUGIN_RESIZE,
} from './data/constants';
@@ -32,6 +34,10 @@ 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 () => {
const title = 'test plugin';
const component = (
@@ -90,3 +96,21 @@ describe('PluginContainer', () => {
expect(iframeElement.attributes.getNamedItem('class').value).toEqual('border border-0');
});
});
+
+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', () => {
+ const component = (
+ Something went wrong}>
+
+ { failingMap }
+
+
+ );
+
+ const { container } = render(component);
+ console.log(container.children);
+ expect(container.firstChild).toHaveTextContent('Something went wrong');
+ });
+});
diff --git a/src/plugins/PluginContainer.jsx b/src/plugins/PluginContainer.jsx
index 9cc95c1f7..4d889d7ec 100644
--- a/src/plugins/PluginContainer.jsx
+++ b/src/plugins/PluginContainer.jsx
@@ -1,7 +1,9 @@
+'use client';
+
import React from 'react';
+// eslint-disable-next-line import/no-extraneous-dependencies
import PluginContainerIframe from './PluginContainerIframe';
-import PluginErrorBoundary from './PluginErrorBoundary';
import {
IFRAME_PLUGIN,
@@ -13,6 +15,7 @@ export default function PluginContainer({ config, ...props }) {
return null;
}
+ // this will allow for future plugin types to be inserted in the PluginErrorBoundary
let renderer = null;
switch (config.type) {
case IFRAME_PLUGIN:
@@ -25,9 +28,7 @@ export default function PluginContainer({ config, ...props }) {
}
return (
-
- {renderer}
-
+ renderer
);
}