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.'); }); });