Skip to content

Commit

Permalink
test: write tests for Plugin component error boundary and fallback me…
Browse files Browse the repository at this point in the history
…thod
  • Loading branch information
jsnwesson committed Sep 25, 2023
1 parent da6fd58 commit ae323d3
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 24 deletions.
6 changes: 4 additions & 2 deletions example-plugin-app/src/PluginOne.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* eslint react/prop-types: off */

import React from 'react';
import React, { useCallback } from 'react';

Check failure on line 3 in example-plugin-app/src/PluginOne.jsx

View workflow job for this annotation

GitHub Actions / tests

'useCallback' is defined but never used
import { Plugin } from '@edx/frontend-platform/plugins';

function Greeting({ subject }) {
return <div>Hello {subject.toUpperCase()}</div>;
return (
<div>Hello {subject.toUpperCase()}</div>
)

Check failure on line 9 in example-plugin-app/src/PluginOne.jsx

View workflow job for this annotation

GitHub Actions / tests

Missing semicolon
}

function errorFallback(error) {
Expand Down
1 change: 1 addition & 0 deletions example/src/PluginsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function PluginsPage() {
className="d-flex flex-column"
pluginProps={{
className: 'flex-grow-1',
title: 'example plugins',
}}
style={{
height: 400,
Expand Down
12 changes: 8 additions & 4 deletions src/plugins/Plugin.jsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 (
<div>
<h2>
Expand All @@ -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) => {
Expand Down Expand Up @@ -63,7 +67,7 @@ export default function Plugin({
return (
<div className={className} style={finalStyle}>
<ErrorBoundary
FallbackComponent={({ error }) => errorFallbackProp(error)}
FallbackComponent={errorFallback}
onError={logErrorToService}
>
{children}
Expand All @@ -82,7 +86,7 @@ Plugin.propTypes = {

Plugin.defaultProps = {
className: null,
errorFallbackProp: errorFallback,
errorFallbackProp: null,
style: {},
ready: true,
};
104 changes: 86 additions & 18 deletions src/plugins/Plugin.test.jsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 = (
<PluginContainer config={iframeConfig} title={title} fallback={<div>Fallback</div>} />
<PluginContainer config={iframeConfig} title={title} fallback={<div>Loading</div>} />
);

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);
Expand Down Expand Up @@ -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 (
<div>Hello World!</div>
);
}

const errorFallback = () => (
<div>
<p>
Oh geez, this is not good at all.
</p>
<br />
</div>
);

it('should render children if no error', () => {
const component = (
<Plugin errorFallbackProp={errorFallback}>
<HealthyComponent />
</Plugin>
);
const { container } = render(component);
expect(container).toHaveTextContent('Hello World!');
});

it('should throw an error if the child component fails', () => {
const component = (
<Plugin className="bg-light" errorFallbackProp={errorFallback}>
<ExplodingComponent />
</Plugin>
);

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 = (
<Plugin errorFallbackProp={errorFallback}>
<ExplodingComponent />
</Plugin>
);

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 = (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Plugin className="bg-light" ready>
{ failingMap }
</Plugin>
</ErrorBoundary>
<Plugin>
<ExplodingComponent />
</Plugin>
);

const { container } = render(component);
expect(container.firstChild).toHaveTextContent('Something went wrong');
expect(container).toHaveTextContent('Oops! An error occurred.');
});
});

0 comments on commit ae323d3

Please sign in to comment.