From 17e57a5a0bf97c98e153b1115ed0322d12a41a7b Mon Sep 17 00:00:00 2001 From: Maxwell Frank Date: Tue, 8 Oct 2024 20:03:30 +0000 Subject: [PATCH 1/6] feat: add errorboundary to pluginslot --- src/plugins/PluginContainer.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/PluginContainer.jsx b/src/plugins/PluginContainer.jsx index 3f7ab403..f8de2f53 100644 --- a/src/plugins/PluginContainer.jsx +++ b/src/plugins/PluginContainer.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { ErrorBoundary } from '@edx/frontend-platform/react'; import PluginContainerIframe from './PluginContainerIframe'; import PluginContainerDirect from './PluginContainerDirect'; @@ -17,6 +18,7 @@ function PluginContainer({ config, slotOptions, ...props }) { return null; } + // TODO: start here and maybe have the ErrorBoundary at the Container level?? // this will allow for future plugin types to be inserted in the PluginErrorBoundary let renderer = null; switch (config.type) { From 60d367d7166aa1c703eb9ea6b2d68738184d76d0 Mon Sep 17 00:00:00 2001 From: Maxwell Frank Date: Fri, 11 Oct 2024 16:35:34 +0000 Subject: [PATCH 2/6] feat: ErrorBoundary for PluginContainer --- .../PluginSlotWithInsert.jsx | 0 .../PluginSlotWithModifyDefaultContents.jsx | 0 .../PluginSlotWithModifyWrapHide.jsx | 0 .../PluginSlotWithModularPlugins.jsx | 0 .../PluginSlotWithoutDefault.jsx | 0 src/plugins/PluginContainer.jsx | 19 ++- src/plugins/PluginContainer.test.jsx | 119 ++++++++++++++++++ src/plugins/PluginSlot.jsx | 9 +- src/plugins/PluginSlot.test.jsx | 3 +- src/setupTest.js | 29 +++++ 10 files changed, 173 insertions(+), 6 deletions(-) rename example/src/{pluginSlots => plugin-slots}/PluginSlotWithInsert.jsx (100%) rename example/src/{pluginSlots => plugin-slots}/PluginSlotWithModifyDefaultContents.jsx (100%) rename example/src/{pluginSlots => plugin-slots}/PluginSlotWithModifyWrapHide.jsx (100%) rename example/src/{pluginSlots => plugin-slots}/PluginSlotWithModularPlugins.jsx (100%) rename example/src/{pluginSlots => plugin-slots}/PluginSlotWithoutDefault.jsx (100%) create mode 100644 src/plugins/PluginContainer.test.jsx diff --git a/example/src/pluginSlots/PluginSlotWithInsert.jsx b/example/src/plugin-slots/PluginSlotWithInsert.jsx similarity index 100% rename from example/src/pluginSlots/PluginSlotWithInsert.jsx rename to example/src/plugin-slots/PluginSlotWithInsert.jsx diff --git a/example/src/pluginSlots/PluginSlotWithModifyDefaultContents.jsx b/example/src/plugin-slots/PluginSlotWithModifyDefaultContents.jsx similarity index 100% rename from example/src/pluginSlots/PluginSlotWithModifyDefaultContents.jsx rename to example/src/plugin-slots/PluginSlotWithModifyDefaultContents.jsx diff --git a/example/src/pluginSlots/PluginSlotWithModifyWrapHide.jsx b/example/src/plugin-slots/PluginSlotWithModifyWrapHide.jsx similarity index 100% rename from example/src/pluginSlots/PluginSlotWithModifyWrapHide.jsx rename to example/src/plugin-slots/PluginSlotWithModifyWrapHide.jsx diff --git a/example/src/pluginSlots/PluginSlotWithModularPlugins.jsx b/example/src/plugin-slots/PluginSlotWithModularPlugins.jsx similarity index 100% rename from example/src/pluginSlots/PluginSlotWithModularPlugins.jsx rename to example/src/plugin-slots/PluginSlotWithModularPlugins.jsx diff --git a/example/src/pluginSlots/PluginSlotWithoutDefault.jsx b/example/src/plugin-slots/PluginSlotWithoutDefault.jsx similarity index 100% rename from example/src/pluginSlots/PluginSlotWithoutDefault.jsx rename to example/src/plugin-slots/PluginSlotWithoutDefault.jsx diff --git a/src/plugins/PluginContainer.jsx b/src/plugins/PluginContainer.jsx index f8de2f53..e2f614be 100644 --- a/src/plugins/PluginContainer.jsx +++ b/src/plugins/PluginContainer.jsx @@ -13,12 +13,13 @@ import { } from './data/constants'; import { pluginConfigShape, slotOptionsShape } from './data/shapes'; -function PluginContainer({ config, slotOptions, ...props }) { +function PluginContainer({ + config, slotOptions, slotErrorFallbackComponent, ...props +}) { if (!config) { return null; } - // TODO: start here and maybe have the ErrorBoundary at the Container level?? // this will allow for future plugin types to be inserted in the PluginErrorBoundary let renderer = null; switch (config.type) { @@ -43,7 +44,16 @@ function PluginContainer({ config, slotOptions, ...props }) { break; } - return renderer; + // Retrieve a fallback component from JS config if one exists + // Otherwise, use the fallback component specific to the PluginSlot if one exists + // Otherwise, default to fallback from frontend-platform's ErrorBoundary + const finalFallback = config.errorFallbackComponent || slotErrorFallbackComponent; + + return ( + + {renderer} + + ); } export default PluginContainer; @@ -53,9 +63,12 @@ PluginContainer.propTypes = { config: PropTypes.shape(pluginConfigShape), /** Options passed to the PluginSlot */ slotOptions: PropTypes.shape(slotOptionsShape), + /** Error fallback component for the PluginSlot */ + slotErrorFallbackComponent: PropTypes.elementType, }; PluginContainer.defaultProps = { config: null, slotOptions: {}, + slotErrorFallbackComponent: React.Fragment, }; diff --git a/src/plugins/PluginContainer.test.jsx b/src/plugins/PluginContainer.test.jsx new file mode 100644 index 00000000..05008a54 --- /dev/null +++ b/src/plugins/PluginContainer.test.jsx @@ -0,0 +1,119 @@ +/* eslint-disable react/prop-types */ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import PluginContainer from './PluginContainer'; +import { IFRAME_PLUGIN, DIRECT_PLUGIN } from './data/constants'; +import PluginContainerDirect from './PluginContainerDirect'; + +jest.mock('./PluginContainerIframe', () => jest.fn(() => 'Iframe plugin')); + +jest.mock('./PluginContainerDirect', () => jest.fn(() => 'Direct plugin')); + +// TODO: figure out how to mock that is imported into the ErrorBoundary component +// This is causing issues with i18n when it tries to render +// Options: +// mock the whole ErrorBoundary component and have it return a mockErrorPage instead +// find if there is a way to mock the import from that happens in + +// Feels perhaps best to just mock the ErrorBoundary here in FPF +// IF this were an MFE, we could use the initializeMockApp helper function from frontend-platform +// since FPF is not an MFE, that mock won't work for us here + +// There may be use cases in the future for testing this ErrorBoundary so perhaps there is value in mocking it here + +// jest.mock('@edx/frontend-platform/react', () ({ +// ...jest.requireActual, +// ErrorBoundary: ({children}) => +// })) + +jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), +})); + +const mockConfig = { + id: 'test-plugin-container', + errorFallbackComponent: undefined, +}; + +function PluginContainerWrapper({ type = DIRECT_PLUGIN, config = mockConfig, slotErrorFallbackComponent = false }) { + return ( + + + + ); +} + +describe('PluginContainer', () => { + // TODO: test for each error boundary + // it renders from the JS config if it exists + // it renders from the PluginSlot if it exists + // it renders the default if no config or PluginSlot fallback are provided + + it('renders a PluginContainerIframe when passed IFRAME_PLUGIN in the configuration', () => { + const { getByText } = render(); + + expect(getByText('Iframe plugin')).toBeInTheDocument(); + }); + + it('renders a PluginContainerDirect when passed DIRECT_PLUGIN in the configuration', () => { + const { getByText } = render(); + + expect(getByText('Direct plugin')).toBeInTheDocument(); + }); + + describe('ErrorBoundary', () => { + beforeAll(() => { + const ExplodingComponent = () => { + throw new Error('an error occurred'); + }; + PluginContainerDirect.mockReturnValue(); + }); + it('renders fallback component from JS config if one exists', () => { + function CustomFallbackFromJSConfig() { + return ( +
+ JS config fallback +
+ ); + } + + const { getByText } = render( + , + }} + />, + ); + expect(getByText('JS config fallback')).toBeInTheDocument(); + }); + + it('renders fallback component from PluginSlot props if one exists', () => { + function CustomFallbackFromPluginSlot() { + return ( +
+ PluginSlot props fallback +
+ ); + } + + const { getByText } = render( + } + />, + ); + expect(getByText('PluginSlot props fallback')).toBeInTheDocument(); + }); + + it('renders default fallback when there is no fallback set in configuration', () => { + const { getByRole } = render(); + expect(getByRole('button', { name: 'Try Again' })).toBeInTheDocument(); + }); + }); +}); diff --git a/src/plugins/PluginSlot.jsx b/src/plugins/PluginSlot.jsx index 8bd87a35..2a891332 100644 --- a/src/plugins/PluginSlot.jsx +++ b/src/plugins/PluginSlot.jsx @@ -18,6 +18,7 @@ const PluginSlot = forwardRef(({ id, pluginProps, slotOptions, + errorFallbackComponent, ...props }, ref) => { /** the plugins below are obtained by the id passed into PluginSlot by the Host MFE. See example/src/PluginsPage.jsx @@ -40,8 +41,8 @@ const PluginSlot = forwardRef(({ const finalPlugins = React.useMemo(() => organizePlugins(defaultContents, plugins), [defaultContents, plugins]); - // TODO: APER-3178 — Unique plugin props - // https://2u-internal.atlassian.net/browse/APER-3178 + // TODO: Unique plugin props + // https://github.com/openedx/frontend-plugin-framework/issues/72 const { loadingFallback } = pluginProps; const defaultLoadingFallback = ( @@ -81,6 +82,7 @@ const PluginSlot = forwardRef(({ key={pluginConfig.id} config={pluginConfig} loadingFallback={finalLoadingFallback} + errorFallbackComponent={errorFallbackComponent} slotOptions={slotOptions} {...pluginProps} /> @@ -125,6 +127,8 @@ PluginSlot.propTypes = { pluginProps: PropTypes.shape(), /** Options passed to the PluginSlot */ slotOptions: PropTypes.shape(slotOptionsShape), + /** Error fallback component to use for each plugin */ + errorFallbackComponent: PropTypes.elementType, }; PluginSlot.defaultProps = { @@ -132,4 +136,5 @@ PluginSlot.defaultProps = { children: null, pluginProps: {}, slotOptions: {}, + errorFallbackComponent: undefined, }; diff --git a/src/plugins/PluginSlot.test.jsx b/src/plugins/PluginSlot.test.jsx index 2a487059..e18244ac 100644 --- a/src/plugins/PluginSlot.test.jsx +++ b/src/plugins/PluginSlot.test.jsx @@ -54,7 +54,8 @@ const pluginContentOnClick = jest.fn(); const defaultContentsOnClick = jest.fn(); const mockOnClick = jest.fn(); -// TODO: APER-3119 — Write unit tests for plugin scenarios not already tested for https://2u-internal.atlassian.net/browse/APER-3119 +// TODO: https://github.com/openedx/frontend-plugin-framework/issues/73 + const content = { text: 'This is a widget.' }; function DefaultContents({ className, onClick, ...rest }) { const handleOnClick = (e) => { diff --git a/src/setupTest.js b/src/setupTest.js index 34bd72ce..d04aadc1 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -1,2 +1,31 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; +import { configure as configureI18n } from '@edx/frontend-platform/i18n'; +import { configure as configureLogging, MockLoggingService } from '@edx/frontend-platform/logging'; +import { getConfig, mergeConfig } from '@edx/frontend-platform'; +import { configure as configureAuth, MockAuthService } from '@edx/frontend-platform/auth'; + +// eslint-disable-next-line import/prefer-default-export +export function initializeMockApp() { + mergeConfig({ + authenticatedUser: { + userId: 'abc123', + username: 'Mock User', + roles: [], + administrator: false, + }, + // ...envConfig, + }); + + const loggingService = configureLogging(MockLoggingService, { + config: {}, + }); + + const i18nService = configureI18n({ + config: {}, + loggingService, + }); + + const authService = configureAuth(MockAuthService, { config: {}, loggingService }); + return { loggingService, i18nService, authService }; +} From d57e0221387da964ff6dac3099634d66230dc430 Mon Sep 17 00:00:00 2001 From: Maxwell Frank Date: Tue, 22 Oct 2024 15:34:33 +0000 Subject: [PATCH 3/6] feat: errorboundary for PluginContainer --- README.rst | 62 ++++++++++++++++++++++++---- src/plugins/PluginContainer.jsx | 4 +- src/plugins/PluginContainer.test.jsx | 36 +++++----------- src/plugins/PluginSlot.jsx | 8 ++-- src/test/MockErrorBoundary.jsx | 49 ++++++++++++++++++++++ 5 files changed, 118 insertions(+), 41 deletions(-) create mode 100644 src/test/MockErrorBoundary.jsx diff --git a/README.rst b/README.rst index 3494872b..397ebde4 100644 --- a/README.rst +++ b/README.rst @@ -168,12 +168,12 @@ If you need to use a plugin operation (e.g. Wrap, Hide, Modify) on default conte Note: The default content will have a priority of 50, allowing for any plugins to appear before or after the default content. Plugin Operations -````````````````` +================= There are four plugin operations that each require specific properties. Insert a Direct Plugin -'''''''''''''''''''''' +`````````````````````` The Insert operation will add a widget in the plugin slot. The contents required for a Direct Plugin is the same as is demonstrated in the Default Contents section above, with the ``content`` key being optional. @@ -196,7 +196,7 @@ is demonstrated in the Default Contents section above, with the ``content`` key } Insert an iFrame Plugin -''''''''''''''''''''''' +``````````````````````` The Insert operation will add a widget in the plugin slot. The contents required for an iFrame Plugin is the same as is demonstrated in the Default Contents section above. @@ -220,7 +220,7 @@ is demonstrated in the Default Contents section above. } Modify -'''''' +`````` The Modify operation allows us to modify the contents of a widget, including its id, type, content, RenderWidget function, or its priority. The operation requires the id of the widget that will be modified and a function to make those changes. @@ -248,7 +248,7 @@ or its priority. The operation requires the id of the widget that will be modifi } Wrap -'''' +```` Unlike Modify, the Wrap operation adds a React component around the widget, and a single widget can receive more than one wrap operation. Each wrapper function takes in a ``component`` and ``id`` prop. @@ -276,7 +276,7 @@ one wrap operation. Each wrapper function takes in a ``component`` and ``id`` pr } Hide -'''' +```` The Hide operation will simply hide whatever content is desired. This is generally used for the default content. @@ -292,14 +292,58 @@ The Hide operation will simply hide whatever content is desired. This is general widgetId: 'some_undesired_plugin', } -Using a Child Micro-frontend (MFE) for iFrame-based Plugins and Fallback Behavior ---------------------------------------------------------------------------------- +Using a Child Micro-frontend (MFE) for iFrame-based Plugins +----------------------------------------------------------- -The Child MFE is no different than any other MFE except that it can define a component that can then be pass into the Host MFE +The Child MFE is no different than any other MFE except that it can define a `Plugin` component that can then be pass into the Host MFE as an iFrame-based plugin via a route. This component communicates (via ``postMessage``) with the Host MFE and resizes its content to match the dimensions available in the Host's plugin slot. +Fallback Behavior +----------------- + +Setting a Fallback component +'''''''''''''''''''''''''''' +The two main places to configure a fallback component for a given implementation are in the PluginSlot props and in the JS configuration. The JS configuration fallback will be prioritized over the PluginSlot props fallback. + +PluginSlot props +```````````````` +Can be used when setting a fallback for the slot that will be used for all of its child plugins. To configure, set the `slotErrorFallbackComponent` prop in the `PluginSlot` to a React component. This will replace the default `` from frontend-platform. + + .. code-block:: + } + /> + +JS configuration +```````````````` +Can be used when setting a fallback for a specific plugin within a slot. Set the `errorFallbackComponent` field for the specific plugin to the custom fallback component in the JS configuration. This will be prioritized over any other fallback components. + + .. code-block:: + const config = { + pluginSlots: { + my_plugin_slot: { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'this_is_a_plugin', + type: DIRECT_PLUGIN, + priority: 60, + RenderWidget: ReactPluginComponent, + errorFallbackComponent: MyCustomFallbackComponent, + }, + }, + ], + }, + }, + }; + +iFrame-based Plugins +'''''''''''''''''''' It's notoriously difficult to know in the Host MFE when an iFrame has failed to load. Because of security sandboxing, the host isn't allowed to know the HTTP status of the request or to inspect what was loaded, so we have to rely on waiting for a ``postMessage`` event from within the iFrame to know it has successfully loaded. diff --git a/src/plugins/PluginContainer.jsx b/src/plugins/PluginContainer.jsx index e2f614be..16d09319 100644 --- a/src/plugins/PluginContainer.jsx +++ b/src/plugins/PluginContainer.jsx @@ -64,11 +64,11 @@ PluginContainer.propTypes = { /** Options passed to the PluginSlot */ slotOptions: PropTypes.shape(slotOptionsShape), /** Error fallback component for the PluginSlot */ - slotErrorFallbackComponent: PropTypes.elementType, + slotErrorFallbackComponent: PropTypes.node, }; PluginContainer.defaultProps = { config: null, slotOptions: {}, - slotErrorFallbackComponent: React.Fragment, + slotErrorFallbackComponent: undefined, }; diff --git a/src/plugins/PluginContainer.test.jsx b/src/plugins/PluginContainer.test.jsx index 05008a54..e5412395 100644 --- a/src/plugins/PluginContainer.test.jsx +++ b/src/plugins/PluginContainer.test.jsx @@ -7,27 +7,16 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import PluginContainer from './PluginContainer'; import { IFRAME_PLUGIN, DIRECT_PLUGIN } from './data/constants'; import PluginContainerDirect from './PluginContainerDirect'; +import MockErrorBoundary from '../test/MockErrorBoundary'; jest.mock('./PluginContainerIframe', () => jest.fn(() => 'Iframe plugin')); jest.mock('./PluginContainerDirect', () => jest.fn(() => 'Direct plugin')); -// TODO: figure out how to mock that is imported into the ErrorBoundary component -// This is causing issues with i18n when it tries to render -// Options: -// mock the whole ErrorBoundary component and have it return a mockErrorPage instead -// find if there is a way to mock the import from that happens in - -// Feels perhaps best to just mock the ErrorBoundary here in FPF -// IF this were an MFE, we could use the initializeMockApp helper function from frontend-platform -// since FPF is not an MFE, that mock won't work for us here - -// There may be use cases in the future for testing this ErrorBoundary so perhaps there is value in mocking it here - -// jest.mock('@edx/frontend-platform/react', () ({ -// ...jest.requireActual, -// ErrorBoundary: ({children}) => -// })) +jest.mock('@edx/frontend-platform/react', () => ({ + ...jest.requireActual, + ErrorBoundary: (props) => , +})); jest.mock('@edx/frontend-platform/logging', () => ({ logError: jest.fn(), @@ -38,7 +27,7 @@ const mockConfig = { errorFallbackComponent: undefined, }; -function PluginContainerWrapper({ type = DIRECT_PLUGIN, config = mockConfig, slotErrorFallbackComponent = false }) { +function PluginContainerWrapper({ type = DIRECT_PLUGIN, config = mockConfig, slotErrorFallbackComponent }) { return ( { - // TODO: test for each error boundary - // it renders from the JS config if it exists - // it renders from the PluginSlot if it exists - // it renders the default if no config or PluginSlot fallback are provided - - it('renders a PluginContainerIframe when passed IFRAME_PLUGIN in the configuration', () => { + it('renders a PluginContainerIframe when passed the IFRAME_PLUGIN type in the configuration', () => { const { getByText } = render(); expect(getByText('Iframe plugin')).toBeInTheDocument(); }); - it('renders a PluginContainerDirect when passed DIRECT_PLUGIN in the configuration', () => { + it('renders a PluginContainerDirect when passed the DIRECT_PLUGIN type in the configuration', () => { const { getByText } = render(); expect(getByText('Direct plugin')).toBeInTheDocument(); @@ -112,8 +96,8 @@ describe('PluginContainer', () => { }); it('renders default fallback when there is no fallback set in configuration', () => { - const { getByRole } = render(); - expect(getByRole('button', { name: 'Try Again' })).toBeInTheDocument(); + const { getByText } = render(); + expect(getByText('Try again')).toBeInTheDocument(); }); }); }); diff --git a/src/plugins/PluginSlot.jsx b/src/plugins/PluginSlot.jsx index 2a891332..7bb55eda 100644 --- a/src/plugins/PluginSlot.jsx +++ b/src/plugins/PluginSlot.jsx @@ -18,7 +18,7 @@ const PluginSlot = forwardRef(({ id, pluginProps, slotOptions, - errorFallbackComponent, + slotErrorFallbackComponent, ...props }, ref) => { /** the plugins below are obtained by the id passed into PluginSlot by the Host MFE. See example/src/PluginsPage.jsx @@ -82,7 +82,7 @@ const PluginSlot = forwardRef(({ key={pluginConfig.id} config={pluginConfig} loadingFallback={finalLoadingFallback} - errorFallbackComponent={errorFallbackComponent} + slotErrorFallbackComponent={slotErrorFallbackComponent} slotOptions={slotOptions} {...pluginProps} /> @@ -128,7 +128,7 @@ PluginSlot.propTypes = { /** Options passed to the PluginSlot */ slotOptions: PropTypes.shape(slotOptionsShape), /** Error fallback component to use for each plugin */ - errorFallbackComponent: PropTypes.elementType, + slotErrorFallbackComponent: PropTypes.node, }; PluginSlot.defaultProps = { @@ -136,5 +136,5 @@ PluginSlot.defaultProps = { children: null, pluginProps: {}, slotOptions: {}, - errorFallbackComponent: undefined, + slotErrorFallbackComponent: undefined, }; diff --git a/src/test/MockErrorBoundary.jsx b/src/test/MockErrorBoundary.jsx new file mode 100644 index 00000000..8b5a74f4 --- /dev/null +++ b/src/test/MockErrorBoundary.jsx @@ -0,0 +1,49 @@ +/** FOR TESTING PURPOSES ONLY */ +/** This mock is used to mock the ErrorBoundary component to avoid having to deal with in testing */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { logError } from '@edx/frontend-platform/logging'; + +/** + * Error boundary component used to log caught errors and display the error page. + * + * @memberof module:React + * @extends {Component} + */ +class MockErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error, info) { + logError(error, { stack: info.componentStack }); + } + + render() { + if (this.state.hasError) { + // Render "Try again" instead of from frontend-platform + return this.props.fallbackComponent ||
Try again
; + } + return this.props.children; + } +} + +MockErrorBoundary.propTypes = { + children: PropTypes.node, + fallbackComponent: PropTypes.node, +}; + +MockErrorBoundary.defaultProps = { + children: null, + fallbackComponent: undefined, +}; + +export default MockErrorBoundary; From a6d63d0e0fc1e08cf0e1dc919849df782a1eb767 Mon Sep 17 00:00:00 2001 From: Maxwell Frank Date: Tue, 22 Oct 2024 15:54:10 +0000 Subject: [PATCH 4/6] fix: remove unused code in setupTest --- src/setupTest.js | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/setupTest.js b/src/setupTest.js index d04aadc1..34bd72ce 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -1,31 +1,2 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; -import { configure as configureI18n } from '@edx/frontend-platform/i18n'; -import { configure as configureLogging, MockLoggingService } from '@edx/frontend-platform/logging'; -import { getConfig, mergeConfig } from '@edx/frontend-platform'; -import { configure as configureAuth, MockAuthService } from '@edx/frontend-platform/auth'; - -// eslint-disable-next-line import/prefer-default-export -export function initializeMockApp() { - mergeConfig({ - authenticatedUser: { - userId: 'abc123', - username: 'Mock User', - roles: [], - administrator: false, - }, - // ...envConfig, - }); - - const loggingService = configureLogging(MockLoggingService, { - config: {}, - }); - - const i18nService = configureI18n({ - config: {}, - loggingService, - }); - - const authService = configureAuth(MockAuthService, { config: {}, loggingService }); - return { loggingService, i18nService, authService }; -} From c3c816ce0b9e83ed5ed49687096b70db3dd7abb1 Mon Sep 17 00:00:00 2001 From: Maxwell Frank Date: Wed, 23 Oct 2024 17:18:14 +0000 Subject: [PATCH 5/6] fix: remove MockErrorBoundary --- src/plugins/PluginContainer.test.jsx | 24 +++++++------- src/test/MockErrorBoundary.jsx | 49 ---------------------------- 2 files changed, 11 insertions(+), 62 deletions(-) delete mode 100644 src/test/MockErrorBoundary.jsx diff --git a/src/plugins/PluginContainer.test.jsx b/src/plugins/PluginContainer.test.jsx index e5412395..da46812d 100644 --- a/src/plugins/PluginContainer.test.jsx +++ b/src/plugins/PluginContainer.test.jsx @@ -2,20 +2,20 @@ import React from 'react'; import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; -import { IntlProvider } from '@edx/frontend-platform/i18n'; import PluginContainer from './PluginContainer'; import { IFRAME_PLUGIN, DIRECT_PLUGIN } from './data/constants'; import PluginContainerDirect from './PluginContainerDirect'; -import MockErrorBoundary from '../test/MockErrorBoundary'; jest.mock('./PluginContainerIframe', () => jest.fn(() => 'Iframe plugin')); jest.mock('./PluginContainerDirect', () => jest.fn(() => 'Direct plugin')); -jest.mock('@edx/frontend-platform/react', () => ({ - ...jest.requireActual, - ErrorBoundary: (props) => , +jest.mock('@edx/frontend-platform/i18n', () => ({ + getLocale: jest.fn(), + getMessages: jest.fn(), + FormattedMessage: ({ defaultMessage }) => defaultMessage, + IntlProvider: ({ children }) =>
{children}
, })); jest.mock('@edx/frontend-platform/logging', () => ({ @@ -29,12 +29,10 @@ const mockConfig = { function PluginContainerWrapper({ type = DIRECT_PLUGIN, config = mockConfig, slotErrorFallbackComponent }) { return ( - - - + ); } @@ -96,8 +94,8 @@ describe('PluginContainer', () => { }); it('renders default fallback when there is no fallback set in configuration', () => { - const { getByText } = render(); - expect(getByText('Try again')).toBeInTheDocument(); + const { getByRole } = render(); + expect(getByRole('button', { name: 'Try again' })).toBeInTheDocument(); }); }); }); diff --git a/src/test/MockErrorBoundary.jsx b/src/test/MockErrorBoundary.jsx deleted file mode 100644 index 8b5a74f4..00000000 --- a/src/test/MockErrorBoundary.jsx +++ /dev/null @@ -1,49 +0,0 @@ -/** FOR TESTING PURPOSES ONLY */ -/** This mock is used to mock the ErrorBoundary component to avoid having to deal with in testing */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { logError } from '@edx/frontend-platform/logging'; - -/** - * Error boundary component used to log caught errors and display the error page. - * - * @memberof module:React - * @extends {Component} - */ -class MockErrorBoundary extends Component { - constructor(props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError() { - // Update state so the next render will show the fallback UI. - return { hasError: true }; - } - - componentDidCatch(error, info) { - logError(error, { stack: info.componentStack }); - } - - render() { - if (this.state.hasError) { - // Render "Try again" instead of from frontend-platform - return this.props.fallbackComponent ||
Try again
; - } - return this.props.children; - } -} - -MockErrorBoundary.propTypes = { - children: PropTypes.node, - fallbackComponent: PropTypes.node, -}; - -MockErrorBoundary.defaultProps = { - children: null, - fallbackComponent: undefined, -}; - -export default MockErrorBoundary; From 9da0e3603fce1536e417b6d7f5767928b0449931 Mon Sep 17 00:00:00 2001 From: Maxwell Frank <92897870+MaxFrank13@users.noreply.github.com> Date: Thu, 31 Oct 2024 09:29:01 -0400 Subject: [PATCH 6/6] chore: update README.rst Co-authored-by: Jason Wesson --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 397ebde4..21e4c60b 100644 --- a/README.rst +++ b/README.rst @@ -309,7 +309,7 @@ The two main places to configure a fallback component for a given implementation PluginSlot props ```````````````` -Can be used when setting a fallback for the slot that will be used for all of its child plugins. To configure, set the `slotErrorFallbackComponent` prop in the `PluginSlot` to a React component. This will replace the default `` from frontend-platform. +This is ideally used when the same fallback should be applied to all of the plugins in the `PluginSlot`. To configure, set the `slotErrorFallbackComponent` prop in the `PluginSlot` to a React component. This will replace the default `` component from frontend-platform. .. code-block::