Skip to content

Commit 7a07c6a

Browse files
committed
test: write tests for Plugin component error boundary and fallback method
1 parent da6fd58 commit 7a07c6a

File tree

4 files changed

+98
-23
lines changed

4 files changed

+98
-23
lines changed

example-plugin-app/src/PluginOne.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import React from 'react';
44
import { Plugin } from '@edx/frontend-platform/plugins';
55

66
function Greeting({ subject }) {
7-
return <div>Hello {subject.toUpperCase()}</div>;
7+
return (
8+
<div>Hello {subject.toUpperCase()}</div>
9+
);
810
}
911

1012
function errorFallback(error) {

example/src/PluginsPage.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default function PluginsPage() {
2424
className="d-flex flex-column"
2525
pluginProps={{
2626
className: 'flex-grow-1',
27+
title: 'example plugins',
2728
}}
2829
style={{
2930
height: 400,

src/plugins/Plugin.jsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use client';
22

3-
import React, { useEffect, useMemo, useState } from 'react';
3+
import React, {
4+
useEffect, useMemo, useState,
5+
} from 'react';
46
import PropTypes from 'prop-types';
57
import { ErrorBoundary } from 'react-error-boundary';
68
import {
@@ -10,7 +12,7 @@ import { logError } from '../logging';
1012
import { PLUGIN_RESIZE } from './data/constants';
1113

1214
// see example-plugin-app/src/PluginOne.jsx for example of customizing errorFallback
13-
function errorFallback() {
15+
function errorFallbackDefault() {
1416
return (
1517
<div>
1618
<h2>
@@ -33,6 +35,8 @@ export default function Plugin({
3335
...style,
3436
}), [dimensions, style]);
3537

38+
const errorFallback = errorFallbackProp || errorFallbackDefault;
39+
3640
// Error logging function
3741
// Need to confirm: When an error is caught here, the logging will be sent to the child MFE's logging service
3842
const logErrorToService = (error, info) => {
@@ -63,7 +67,7 @@ export default function Plugin({
6367
return (
6468
<div className={className} style={finalStyle}>
6569
<ErrorBoundary
66-
FallbackComponent={({ error }) => errorFallbackProp(error)}
70+
FallbackComponent={errorFallback}
6771
onError={logErrorToService}
6872
>
6973
{children}
@@ -82,7 +86,7 @@ Plugin.propTypes = {
8286

8387
Plugin.defaultProps = {
8488
className: null,
85-
errorFallbackProp: errorFallback,
89+
errorFallbackProp: null,
8690
style: {},
8791
ready: true,
8892
};

src/plugins/Plugin.test.jsx

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
/* eslint react/prop-types: off */
2+
13
import React from 'react';
2-
import { ErrorBoundary } from 'react-error-boundary';
34
import { render } from '@testing-library/react';
45
import { fireEvent } from '@testing-library/dom';
56
import '@testing-library/jest-dom';
67

8+
import { initializeMockApp } from '..';
79
import PluginContainer from './PluginContainer';
810
import Plugin from './Plugin';
911
import {
@@ -34,25 +36,21 @@ describe('PluginContainer', () => {
3436
expect(container.firstChild).toBeNull();
3537
});
3638

37-
it('should render the desired fallback when the iframe fails to render', () => {
38-
39-
});
40-
41-
it('should render a PluginIFrame when given an iFrame config', async () => {
39+
it('should render a Plugin iFrame Container when given an iFrame config', async () => {
4240
const title = 'test plugin';
4341
const component = (
44-
<PluginContainer config={iframeConfig} title={title} fallback={<div>Fallback</div>} />
42+
<PluginContainer config={iframeConfig} title={title} fallback={<div>Loading</div>} />
4543
);
4644

4745
const { container } = render(component);
4846

49-
const iframeElement = await container.querySelector('iframe');
47+
const iframeElement = container.querySelector('iframe');
5048
const fallbackElement = container.querySelector('div');
5149

5250
expect(iframeElement).toBeInTheDocument();
5351
expect(fallbackElement).toBeInTheDocument();
5452

55-
expect(fallbackElement.innerHTML).toEqual('Fallback');
53+
expect(fallbackElement.innerHTML).toEqual('Loading');
5654

5755
// Ensure the iframe has the proper attributes
5856
expect(iframeElement.attributes.getNamedItem('allow').value).toEqual(IFRAME_FEATURE_POLICY);
@@ -98,18 +96,88 @@ describe('PluginContainer', () => {
9896
});
9997

10098
describe('Plugin', () => {
101-
const breakingArray = null;
102-
const failingMap = () => breakingArray.map(a => a);
103-
it('should render the desired fallback when the error boundary receives a React error', () => {
99+
let logError = jest.fn();
100+
101+
beforeEach(async () => {
102+
// This is a gross hack to suppress error logs in the invalid parentSelector test
103+
jest.spyOn(console, 'error');
104+
global.console.error.mockImplementation(() => {});
105+
106+
const { loggingService } = initializeMockApp();
107+
logError = loggingService.logError;
108+
});
109+
110+
afterEach(() => {
111+
global.console.error.mockRestore();
112+
jest.clearAllMocks();
113+
});
114+
115+
const ExplodingComponent = () => {
116+
throw new Error('booyah');
117+
};
118+
119+
function HealthyComponent() {
120+
return (
121+
<div>Hello World!</div>
122+
);
123+
}
124+
125+
const errorFallback = () => (
126+
<div>
127+
<p>
128+
Oh geez, this is not good at all.
129+
</p>
130+
<br />
131+
</div>
132+
);
133+
134+
it('should render children if no error', () => {
135+
const component = (
136+
<Plugin errorFallbackProp={errorFallback}>
137+
<HealthyComponent />
138+
</Plugin>
139+
);
140+
const { container } = render(component);
141+
expect(container).toHaveTextContent('Hello World!');
142+
});
143+
144+
it('should throw an error if the child component fails', () => {
145+
const component = (
146+
<Plugin className="bg-light" errorFallbackProp={errorFallback}>
147+
<ExplodingComponent />
148+
</Plugin>
149+
);
150+
151+
render(component);
152+
153+
expect(logError).toHaveBeenCalledTimes(1);
154+
expect(logError).toHaveBeenCalledWith(
155+
new Error('booyah'),
156+
expect.objectContaining({
157+
stack: expect.stringContaining('ExplodingComponent'),
158+
}),
159+
);
160+
});
161+
162+
it('should render the passed in fallback component when the error boundary receives a React error', () => {
163+
const component = (
164+
<Plugin errorFallbackProp={errorFallback}>
165+
<ExplodingComponent />
166+
</Plugin>
167+
);
168+
169+
const { container } = render(component);
170+
expect(container).toHaveTextContent('Oh geez');
171+
});
172+
173+
it('should render the default fallback component when one is not passed into the Plugin', () => {
104174
const component = (
105-
<ErrorBoundary fallback={<div>Something went wrong</div>}>
106-
<Plugin className="bg-light" ready>
107-
{ failingMap }
108-
</Plugin>
109-
</ErrorBoundary>
175+
<Plugin>
176+
<ExplodingComponent />
177+
</Plugin>
110178
);
111179

112180
const { container } = render(component);
113-
expect(container.firstChild).toHaveTextContent('Something went wrong');
181+
expect(container).toHaveTextContent('Oops! An error occurred.');
114182
});
115183
});

0 commit comments

Comments
 (0)