feat(error-handling): introduce unified and configurable error handling (#7761)

Refs #7778
This commit is contained in:
Vladimir Gorej
2022-01-24 16:12:13 +01:00
committed by GitHub
parent 4f2287fe53
commit 8b1c4a7c1a
19 changed files with 629 additions and 295 deletions

View File

@@ -7,7 +7,6 @@ module.exports = {
'**/test/unit/*.js?(x)',
'**/test/unit/**/*.js?(x)',
],
// testMatch: ['**/test/unit/core/plugins/auth/actions.js'],
setupFilesAfterEnv: ['<rootDir>/test/unit/setup.js'],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',

View File

@@ -233,3 +233,168 @@ const ui = SwaggerUIBundle({
...snippetConfig,
})
```
### Error handling
SwaggerUI comes with a `safe-render` plugin that handles error handling allows plugging into error handling system and modify it.
The plugin accepts a list of component names that should be protected by error boundaries.
Its public API looks like this:
```js
{
fn: {
componentDidCatch,
withErrorBoundary: withErrorBoundary(getSystem),
},
components: {
ErrorBoundary,
Fallback,
},
}
```
safe-render plugin is automatically utilized by [base](https://github.com/swagger-api/swagger-ui/blob/78f62c300a6d137e65fd027d850981b010009970/src/core/presets/base.js) and [standalone](https://github.com/swagger-api/swagger-ui/tree/78f62c300a6d137e65fd027d850981b010009970/src/standalone) SwaggerUI presets and
should always be used as the last plugin, after all the components are already known to the SwaggerUI.
The plugin defines a default list of components that should be protected by error boundaries:
```js
[
"App",
"BaseLayout",
"VersionPragmaFilter",
"InfoContainer",
"ServersContainer",
"SchemesContainer",
"AuthorizeBtnContainer",
"FilterContainer",
"Operations",
"OperationContainer",
"parameters",
"responses",
"OperationServers",
"Models",
"ModelWrapper",
"Topbar",
"StandaloneLayout",
"onlineValidatorBadge"
]
```
As demonstrated below, additional components can be protected by utilizing the safe-render plugin
with configuration options. This gets really handy if you are a SwaggerUI integrator and you maintain a number of
plugins with additional custom components.
```js
const swaggerUI = SwaggerUI({
url: "https://petstore.swagger.io/v2/swagger.json",
dom_id: '#swagger-ui',
plugins: [
() => ({
components: {
MyCustomComponent1: () => 'my custom component',
},
}),
SwaggerUI.plugins.SafeRender({
fullOverride: true, // only the component list defined here will apply (not the default list)
componentList: [
"MyCustomComponent1",
],
}),
],
});
```
##### componentDidCatch
This static function is invoked after a component has thrown an error.
It receives two parameters:
1. `error` - The error that was thrown.
2. `info` - An object with a componentStack key containing [information about which component threw the error](https://reactjs.org/docs/error-boundaries.html#component-stack-traces).
It has precisely the same signature as error boundaries [componentDidCatch lifecycle method](https://reactjs.org/docs/react-component.html#componentdidcatch),
except it's a static function and not a class method.
Default implement of componentDidCatch uses `console.error` to display the received error:
```js
export const componentDidCatch = console.error;
```
To utilize your own error handling logic (e.g. [bugsnag](https://www.bugsnag.com/)), create new SwaggerUI plugin that overrides componentDidCatch:
{% highlight js linenos %}
const BugsnagErrorHandlerPlugin = () => {
// init bugsnag
return {
fn: {
componentDidCatch = (error, info) => {
Bugsnag.notify(error);
Bugsnag.notify(info);
},
},
};
};
{% endhighlight %}
##### withErrorBoundary
This function is HOC (Higher Order Component). It wraps a particular component into the `ErrorBoundary` component.
It can be overridden via a plugin system to control how components are wrapped by the ErrorBoundary component.
In 99.9% of situations, you won't need to override this function, but if you do, please read the source code of this function first.
##### Fallback
The component is displayed when the error boundary catches an error. It can be overridden via a plugin system.
Its default implementation is trivial:
```js
import React from "react"
import PropTypes from "prop-types"
const Fallback = ({ name }) => (
<div className="fallback">
😱 <i>Could not render { name === "t" ? "this component" : name }, see the console.</i>
</div>
)
Fallback.propTypes = {
name: PropTypes.string.isRequired,
}
export default Fallback
```
Feel free to override it to match your look & feel:
```js
const CustomFallbackPlugin = () => ({
components: {
Fallback: ({ name } ) => `This is my custom fallback. ${name} failed to render`,
},
});
const swaggerUI = SwaggerUI({
url: "https://petstore.swagger.io/v2/swagger.json",
dom_id: '#swagger-ui',
plugins: [
CustomFallbackPlugin,
]
});
```
##### ErrorBoundary
This is the component that implements React error boundaries. Uses `componentDidCatch` and `Fallback`
under the hood. In 99.9% of situations, you won't need to override this component, but if you do,
please read the source code of this component first.
##### Change in behavior
In prior releases of SwaggerUI (before v4.3.0), almost all components have been protected, and when thrown error,
`Fallback` component was displayed. This changes with SwaggerUI v4.3.0. Only components defined
by the `safe-render` plugin are now protected and display fallback. If a small component somewhere within
SwaggerUI React component tree fails to render and throws an error. The error bubbles up to the closest
error boundary, and that error boundary displays the `Fallback` component and invokes `componentDidCatch`.

View File

@@ -31,7 +31,7 @@ const HighlightCode = ({value, fileName, className, downloadable, getConfigs, ca
}
const handlePreventYScrollingBeyondElement = (e) => {
const { target, deltaY } = e
const { target, deltaY } = e
const { scrollHeight: contentHeight, offsetHeight: visibleHeight, scrollTop } = target
const scrollOffset = visibleHeight + scrollTop
const isElementScrollable = contentHeight > visibleHeight

View File

@@ -28,7 +28,6 @@ export default class BaseLayout extends React.Component {
const SchemesContainer = getComponent("SchemesContainer", true)
const AuthorizeBtnContainer = getComponent("AuthorizeBtnContainer", true)
const FilterContainer = getComponent("FilterContainer", true)
const ErrorBoundary = getComponent("ErrorBoundary", true)
let isSwagger2 = specSelectors.isSwagger2()
let isOAS3 = specSelectors.isOAS3()
@@ -87,40 +86,38 @@ export default class BaseLayout extends React.Component {
return (
<div className='swagger-ui'>
<ErrorBoundary targetName="BaseLayout">
<SvgAssets />
<VersionPragmaFilter isSwagger2={isSwagger2} isOAS3={isOAS3} alsoShow={<Errors/>}>
<Errors/>
<Row className="information-container">
<Col mobile={12}>
<InfoContainer/>
</Col>
</Row>
<SvgAssets />
<VersionPragmaFilter isSwagger2={isSwagger2} isOAS3={isOAS3} alsoShow={<Errors/>}>
<Errors/>
<Row className="information-container">
<Col mobile={12}>
<InfoContainer/>
</Col>
</Row>
{hasServers || hasSchemes || hasSecurityDefinitions ? (
<div className="scheme-container">
<Col className="schemes wrapper" mobile={12}>
{hasServers ? (<ServersContainer />) : null}
{hasSchemes ? (<SchemesContainer />) : null}
{hasSecurityDefinitions ? (<AuthorizeBtnContainer />) : null}
</Col>
</div>
) : null}
<FilterContainer/>
<Row>
<Col mobile={12} desktop={12} >
<Operations/>
{hasServers || hasSchemes || hasSecurityDefinitions ? (
<div className="scheme-container">
<Col className="schemes wrapper" mobile={12}>
{hasServers ? (<ServersContainer />) : null}
{hasSchemes ? (<SchemesContainer />) : null}
{hasSecurityDefinitions ? (<AuthorizeBtnContainer />) : null}
</Col>
</Row>
<Row>
<Col mobile={12} desktop={12} >
<Models/>
</Col>
</Row>
</VersionPragmaFilter>
</ErrorBoundary>
</div>
) : null}
<FilterContainer/>
<Row>
<Col mobile={12} desktop={12} >
<Operations/>
</Col>
</Row>
<Row>
<Col mobile={12} desktop={12} >
<Models/>
</Col>
</Row>
</VersionPragmaFilter>
</div>
)
}

View File

@@ -1,4 +1,5 @@
import { pascalCaseFilename } from "core/utils"
import SafeRender from "core/plugins/safe-render"
const request = require.context(".", true, /\.jsx?$/)
@@ -18,4 +19,6 @@ request.keys().forEach( function( key ){
allPlugins[pascalCaseFilename(key)] = mod.default ? mod.default : mod
})
allPlugins.SafeRender = SafeRender
export default allPlugins

View File

@@ -88,7 +88,7 @@ const RequestBody = ({
const sampleForMediaType = rawExamplesOfMediaType?.map((container, key) => {
const val = container?.get("value", null)
if(val) {
container = container.set("value", getDefaultRequestBodyValue(
container = container.set("value", getDefaultRequestBodyValue(
requestBody,
contentType,
key,

View File

@@ -1,27 +1,28 @@
import PropTypes from "prop-types"
import React, { Component } from "react"
import { componentDidCatch } from "../fn"
import Fallback from "./fallback"
export class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
constructor(...args) {
super(...args)
this.state = { hasError: false, error: null }
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo) // eslint-disable-line no-console
this.props.fn.componentDidCatch(error, errorInfo)
}
render() {
const { getComponent, targetName, children } = this.props
const FallbackComponent = getComponent("Fallback")
if (this.state.hasError) {
const FallbackComponent = getComponent("Fallback")
return <FallbackComponent name={targetName} />
}
@@ -31,6 +32,7 @@ export class ErrorBoundary extends Component {
ErrorBoundary.propTypes = {
targetName: PropTypes.string,
getComponent: PropTypes.func,
fn: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
@@ -39,6 +41,9 @@ ErrorBoundary.propTypes = {
ErrorBoundary.defaultProps = {
targetName: "this component",
getComponent: () => Fallback,
fn: {
componentDidCatch,
},
children: null,
}

View File

@@ -0,0 +1,32 @@
import React, { Component } from "react"
export const componentDidCatch = console.error
const isClassComponent = component => component.prototype && component.prototype.isReactComponent
export const withErrorBoundary = (getSystem) => (WrappedComponent) => {
const { getComponent, fn } = getSystem()
const ErrorBoundary = getComponent("ErrorBoundary")
const targetName = fn.getDisplayName(WrappedComponent)
class WithErrorBoundary extends Component {
render() {
return (
<ErrorBoundary targetName={targetName} getComponent={getComponent} fn={fn}>
<WrappedComponent {...this.props} {...this.context} />
</ErrorBoundary>
)
}
}
WithErrorBoundary.displayName = `WithErrorBoundary(${targetName})`
if (isClassComponent(WrappedComponent)) {
/**
* We need to handle case of class components defining a `mapStateToProps` public method.
* Components with `mapStateToProps` public method cannot be wrapped.
*/
WithErrorBoundary.prototype.mapStateToProps = WrappedComponent.prototype.mapStateToProps
}
return WithErrorBoundary
}

View File

@@ -0,0 +1,42 @@
import zipObject from "lodash/zipObject"
import ErrorBoundary from "./components/error-boundary"
import Fallback from "./components/fallback"
import { componentDidCatch, withErrorBoundary } from "./fn"
const safeRenderPlugin = ({componentList = [], fullOverride = false} = {}) => ({ getSystem }) => {
const defaultComponentList = [
"App",
"BaseLayout",
"VersionPragmaFilter",
"InfoContainer",
"ServersContainer",
"SchemesContainer",
"AuthorizeBtnContainer",
"FilterContainer",
"Operations",
"OperationContainer",
"parameters",
"responses",
"OperationServers",
"Models",
"ModelWrapper",
]
const mergedComponentList = fullOverride ? componentList : [...defaultComponentList, ...componentList]
const wrapFactory = (Original, { fn }) => fn.withErrorBoundary(Original)
const wrapComponents = zipObject(mergedComponentList, Array(mergedComponentList.length).fill(wrapFactory))
return {
fn: {
componentDidCatch,
withErrorBoundary: withErrorBoundary(getSystem),
},
components: {
ErrorBoundary,
Fallback,
},
wrapComponents,
}
}
export default safeRenderPlugin

View File

@@ -0,0 +1 @@
export const getDisplayName = (WrappedComponent) => WrappedComponent.displayName || WrappedComponent.name || "Component"

View File

@@ -1,26 +1,23 @@
import * as rootInjects from "./root-injects"
import { memoize } from "core/utils"
import ErrorBoundary from "./error-boundary"
import Fallback from "./fallback"
export default function({getComponents, getStore, getSystem}) {
let { getComponent, render, makeMappedContainer } = rootInjects
import { getComponent, render, withMappedContainer } from "./root-injects"
import { getDisplayName } from "./fn"
const viewPlugin = ({getComponents, getStore, getSystem}) => {
// getComponent should be passed into makeMappedContainer, _already_ memoized... otherwise we have a big performance hit ( think, really big )
const memGetComponent = memoize(getComponent.bind(null, getSystem, getStore, getComponents))
const memMakeMappedContainer = memoize(makeMappedContainer.bind(null, getSystem, getStore, memGetComponent, getComponents))
const memGetComponent = memoize(getComponent(getSystem, getStore, getComponents))
const memMakeMappedContainer = memoize(withMappedContainer(getSystem, getStore, memGetComponent))
return {
rootInjects: {
getComponent: memGetComponent,
makeMappedContainer: memMakeMappedContainer,
render: render.bind(null, getSystem, getStore, getComponent, getComponents),
render: render(getSystem, getStore, getComponent, getComponents),
},
components: {
ErrorBoundary,
Fallback,
fn: {
getDisplayName,
},
}
}
export default viewPlugin

View File

@@ -1,55 +1,67 @@
import React, { Component } from "react"
import ReactDOM from "react-dom"
import { compose } from "redux"
import { connect, Provider } from "react-redux"
import omit from "lodash/omit"
import identity from "lodash/identity"
const SystemWrapper = (getSystem, ComponentToWrap ) => class extends Component {
render() {
return <ComponentToWrap {...getSystem()} {...this.props} {...this.context} />
const withSystem = (getSystem) => (WrappedComponent) => {
const { fn } = getSystem()
class WithSystem extends Component {
render() {
return <WrappedComponent {...getSystem()} {...this.props} {...this.context} />
}
}
WithSystem.displayName = `WithSystem(${fn.getDisplayName(WrappedComponent)})`
return WithSystem
}
const RootWrapper = (getSystem, reduxStore, ComponentToWrap) => class extends Component {
render() {
const { getComponent } = getSystem()
const ErrorBoundary = getComponent("ErrorBoundary", true)
const withRoot = (getSystem, reduxStore) => (WrappedComponent) => {
const { fn } = getSystem()
return (
<Provider store={reduxStore}>
<ErrorBoundary targetName={ComponentToWrap?.name}>
<ComponentToWrap {...this.props} {...this.context} />
</ErrorBoundary>
</Provider>
)
class WithRoot extends Component {
render() {
return (
<Provider store={reduxStore}>
<WrappedComponent {...this.props} {...this.context} />
</Provider>
)
}
}
WithRoot.displayName = `WithRoot(${fn.getDisplayName(WrappedComponent)})`
return WithRoot
}
const makeContainer = (getSystem, component, reduxStore) => {
const mapStateToProps = function(state, ownProps) {
const propsForContainerComponent = Object.assign({}, ownProps, getSystem())
const ori = component.prototype.mapStateToProps || (state => { return {state} })
return ori(state, propsForContainerComponent)
const withConnect = (getSystem, WrappedComponent, reduxStore) => {
const mapStateToProps = (state, ownProps) => {
const props = {...ownProps, ...getSystem()}
const customMapStateToProps = WrappedComponent.prototype?.mapStateToProps || (state => ({state}))
return customMapStateToProps(state, props)
}
let wrappedWithSystem = SystemWrapper(getSystem, component, reduxStore)
let connected = connect( mapStateToProps )(wrappedWithSystem)
if(reduxStore)
return RootWrapper(getSystem, reduxStore, connected)
return connected
return compose(
reduxStore ? withRoot(getSystem, reduxStore) : identity,
connect(mapStateToProps),
withSystem(getSystem),
)(WrappedComponent)
}
const handleProps = (getSystem, mapping, props, oldProps) => {
for (let prop in mapping) {
let fn = mapping[prop]
if(typeof fn === "function")
for (const prop in mapping) {
const fn = mapping[prop]
if (typeof fn === "function") {
fn(props[prop], oldProps[prop], getSystem())
}
}
}
export const makeMappedContainer = (getSystem, getStore, memGetComponent, getComponents, componentName, mapping) => {
return class extends Component {
export const withMappedContainer = (getSystem, getStore, memGetComponent) => (componentName, mapping) => {
const { fn } = getSystem()
const WrappedComponent = memGetComponent(componentName, "root")
class WithMappedContainer extends Component {
constructor(props, context) {
super(props, context)
handleProps(getSystem, mapping, props, {})
@@ -60,84 +72,44 @@ export const makeMappedContainer = (getSystem, getStore, memGetComponent, getCom
}
render() {
let cleanProps = omit(this.props, mapping ? Object.keys(mapping) : [])
let Comp = memGetComponent(componentName, "root")
return <Comp {...cleanProps}/>
const cleanProps = omit(this.props, mapping ? Object.keys(mapping) : [])
return <WrappedComponent {...cleanProps} />
}
}
WithMappedContainer.displayName = `WithMappedContainer(${fn.getDisplayName(WrappedComponent)})`
return WithMappedContainer
}
export const render = (getSystem, getStore, getComponent, getComponents, domNode) => {
let App = getComponent(getSystem, getStore, getComponents, "App", "root")
export const render = (getSystem, getStore, getComponent, getComponents) => (domNode) => {
const App = getComponent(getSystem, getStore, getComponents)("App", "root")
ReactDOM.render(<App/>, domNode)
}
/**
* Creates a class component from a stateless one and wrap it with Error Boundary
* to handle errors coming from a stateless component.
*/
const createClass = (getSystem, OriginalComponent) => class extends Component {
render() {
const { getComponent } = getSystem()
const ErrorBoundary = getComponent("ErrorBoundary")
export const getComponent = (getSystem, getStore, getComponents) => (componentName, container, config = {}) => {
return (
<ErrorBoundary targetName={OriginalComponent?.name} getComponent={getComponent}>
<OriginalComponent {...this.props} />
</ErrorBoundary>
)
}
}
const wrapRender = (getSystem, component) => {
const isStateless = component => !(component.prototype && component.prototype.isReactComponent)
const target = isStateless(component) ? createClass(getSystem, component) : component
const { render: oriRender} = target.prototype
/**
* This render method override handles errors that are throw in render method
* of class components.
*/
target.prototype.render = function render(...args) {
try {
return oriRender.apply(this, args)
} catch (error) {
const { getComponent } = getSystem()
const Fallback = getComponent("Fallback")
console.error(error) // eslint-disable-line no-console
return <Fallback name={target.name} />
}
}
return target
}
export const getComponent = (getSystem, getStore, getComponents, componentName, container, config = {}) => {
if(typeof componentName !== "string")
if (typeof componentName !== "string")
throw new TypeError("Need a string, to fetch a component. Was given a " + typeof componentName)
// getComponent has a config object as a third, optional parameter
// using the config object requires the presence of the second parameter, container
// e.g. getComponent("JsonSchema_string_whatever", false, { failSilently: true })
let component = getComponents(componentName)
const component = getComponents(componentName)
if(!component) {
if (!component) {
if (!config.failSilently) {
getSystem().log.warn("Could not find component:", componentName)
}
return null
}
if(!container)
return wrapRender(getSystem, component)
if(!container) {
return component
}
if(container === "root")
return makeContainer(getSystem, component, getStore())
if(container === "root") {
return withConnect(getSystem, component, getStore())
}
// container == truthy
return makeContainer(getSystem, wrapRender(getSystem, component))
return withConnect(getSystem, component)
}

View File

@@ -13,6 +13,7 @@ import configsPlugin from "core/plugins/configs"
import deepLinkingPlugin from "core/plugins/deep-linking"
import filter from "core/plugins/filter"
import onComplete from "core/plugins/on-complete"
import safeRender from "core/plugins/safe-render"
import OperationContainer from "core/containers/OperationContainer"
@@ -193,6 +194,7 @@ export default function() {
deepLinkingPlugin,
filter,
onComplete,
requestSnippets
requestSnippets,
safeRender(),
]
}

View File

@@ -1,6 +1,7 @@
import StandaloneLayout from "./layout"
import TopbarPlugin from "plugins/topbar"
import ConfigsPlugin from "corePlugins/configs"
import SafeRenderPlugin from "core/plugins/safe-render"
// the Standalone preset
@@ -11,5 +12,13 @@ export default [
return {
components: { StandaloneLayout }
}
}
},
SafeRenderPlugin({
fullOverride: true,
componentList: [
"Topbar",
"StandaloneLayout",
"onlineValidatorBadge"
]
})
]

View File

@@ -21,22 +21,16 @@ export default class StandaloneLayout extends React.Component {
const Topbar = getComponent("Topbar", true)
const BaseLayout = getComponent("BaseLayout", true)
const OnlineValidatorBadge = getComponent("onlineValidatorBadge", true)
const ErrorBoundary = getComponent("ErrorBoundary", true)
return (
<Container className='swagger-ui'>
<ErrorBoundary targetName="Topbar">
{Topbar ? <Topbar /> : null}
</ErrorBoundary>
{Topbar ? <Topbar /> : null}
<BaseLayout />
<ErrorBoundary targetName="OnlineValidatorBadge">
<Row>
<Col>
<OnlineValidatorBadge />
</Col>
</Row>
</ErrorBoundary>
<Row>
<Col>
<OnlineValidatorBadge />
</Col>
</Row>
</Container>
)
}

View File

@@ -179,7 +179,7 @@ describe("auth plugin - actions", () => {
describe("tokenRequest", function () {
it("should send the code verifier when set", () => {
const testCodeVerifierForAuthorizationCodeFlows = (flowAction) => {
const testCodeVerifierForAuthorizationCodeFlows = (flowAction) => {
const data = {
auth: {
schema: {

View File

@@ -0,0 +1,253 @@
import React from "react"
import { mount } from "enzyme"
import sinon from "sinon"
import { Provider } from "react-redux"
import noop from "lodash/noop"
import System from "core/system"
import ViewPlugin from "core/plugins/view"
import SafeRenderPlugin from "core/plugins/safe-render"
describe("safe-render", function() {
const DisableComponentDidCatchPlugin = () => ({
fn: {
componentDidCatch: noop,
}
})
it("should catch errors thrown inside of React Component class render method", function() {
class BrokenComponent extends React.Component {
render() {
return null
}
}
const BrokenComponentPlugin = () => {
return {
components: {
BrokenComponent,
}
}
}
const system = new System({
plugins: [
ViewPlugin,
BrokenComponentPlugin,
SafeRenderPlugin({
fullOverride: true,
componentList: ["BrokenComponent"],
}),
DisableComponentDidCatchPlugin,
]
})
const SafeBrokenComponent = system.getSystem().getComponent("BrokenComponent")
const wrapper = mount(<SafeBrokenComponent />)
wrapper.find(BrokenComponent).simulateError(new Error("error"))
expect(wrapper.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
it("should catch errors thrown inside of PureComponent class render method", function() {
class BrokenComponent extends React.PureComponent {
render() {
return null
}
}
const BrokenComponentPlugin = () => {
return {
components: {
BrokenComponent,
}
}
}
const system = new System({
plugins: [
ViewPlugin,
BrokenComponentPlugin,
SafeRenderPlugin({
fullOverride: true,
componentList: ["BrokenComponent"],
}),
DisableComponentDidCatchPlugin,
]
})
const SafeBrokenComponent = system.getSystem().getComponent("BrokenComponent")
const wrapper = mount(<SafeBrokenComponent />)
wrapper.find(BrokenComponent).simulateError(new Error("error"))
expect(wrapper.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
it("should catch errors thrown inside of function component", function() {
const BrokenComponent = () => null
const BrokenComponentPlugin = () => {
return {
components: {
BrokenComponent,
}
}
}
const system = new System({
plugins: [
ViewPlugin,
BrokenComponentPlugin,
SafeRenderPlugin({
fullOverride: true,
componentList: ["BrokenComponent"],
}),
DisableComponentDidCatchPlugin,
]
})
const SafeBrokenComponent = system.getSystem().getComponent("BrokenComponent")
const wrapper = mount(<SafeBrokenComponent />)
wrapper.find(BrokenComponent).simulateError(new Error("error"))
expect(wrapper.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
it("should catch errors thrown inside of container created from React Component class", function() {
class BrokenComponent extends React.Component {
render() {
return null
}
}
const BrokenComponentPlugin = () => {
return {
components: {
BrokenComponent,
}
}
}
const system = new System({
plugins: [
ViewPlugin,
BrokenComponentPlugin,
SafeRenderPlugin({
fullOverride: true,
componentList: ["BrokenComponent"],
}),
DisableComponentDidCatchPlugin,
]
})
const SafeBrokenComponent = system.getSystem().getComponent("BrokenComponent", true)
const wrapper = mount(
<Provider store={system.getStore()}>
<SafeBrokenComponent />
</Provider>
)
wrapper.find(BrokenComponent).simulateError(new Error("error"))
expect(wrapper.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
it("should catch errors thrown inside of container created from function component", function() {
const BrokenComponent = () => null
const BrokenComponentPlugin = () => {
return {
components: {
BrokenComponent,
}
}
}
const system = new System({
plugins: [
ViewPlugin,
BrokenComponentPlugin,
SafeRenderPlugin({
fullOverride: true,
componentList: ["BrokenComponent"],
}),
DisableComponentDidCatchPlugin,
]
})
const SafeBrokenComponent = system.getSystem().getComponent("BrokenComponent", true)
const wrapper = mount(
<Provider store={system.getStore()}>
<SafeBrokenComponent />
</Provider>
)
wrapper.find(BrokenComponent).simulateError(new Error("error"))
expect(wrapper.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
it("should render custom Fallback component", function() {
const BrokenComponent = () => null
const BrokenComponentPlugin = () => {
return {
components: {
BrokenComponent,
}
}
}
const FallbackPlugin = () => ({
components: {
Fallback: () => "fallback component",
},
})
const system = new System({
plugins: [
ViewPlugin,
BrokenComponentPlugin,
SafeRenderPlugin({
fullOverride: true,
componentList: ["BrokenComponent"],
}),
FallbackPlugin,
DisableComponentDidCatchPlugin,
]
})
const SafeBrokenComponent = system.getSystem().getComponent("BrokenComponent")
const wrapper = mount(<SafeBrokenComponent />)
wrapper.find(BrokenComponent).simulateError(new Error("error"))
expect(wrapper.text()).toEqual("fallback component")
})
it("should call custom componentDidCatch hook", function() {
const BrokenComponent = () => null
const componentDidCatch = sinon.spy()
const BrokenComponentPlugin = () => {
return {
components: {
BrokenComponent,
}
}
}
const ComponentDidCatchPlugin = () => ({
fn: {
componentDidCatch,
},
})
const system = new System({
plugins: [
ViewPlugin,
BrokenComponentPlugin,
SafeRenderPlugin({
fullOverride: true,
componentList: ["BrokenComponent"],
}),
ComponentDidCatchPlugin,
]
})
const SafeBrokenComponent = system.getSystem().getComponent("BrokenComponent")
const wrapper = mount(<SafeBrokenComponent />)
wrapper.find(BrokenComponent).simulateError(new Error("error"))
expect(componentDidCatch.calledOnce).toBe(true)
})
})

View File

@@ -911,142 +911,5 @@ describe("bound system", function(){
expect(system.getSystem().throwSelectors.func).not.toThrow()
})
describe("components", function() {
it("should catch errors thrown inside of React Component Class render methods", function() {
// Given
class BrokenComponent extends React.Component {
// eslint-disable-next-line react/require-render-return
render() {
throw new Error("This component is broken")
}
}
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
let Component = system.getSystem().getComponent("BrokenComponent")
const renderedComponent = render(<Component />)
// Then
expect(renderedComponent.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
it("should catch errors thrown inside of pure component", function() {
// Given
class BrokenComponent extends PureComponent {
// eslint-disable-next-line react/require-render-return
render() {
throw new Error("This component is broken")
}
}
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
let Component = system.getSystem().getComponent("BrokenComponent")
const wrapper = mount(<Component />)
// Then
expect(wrapper.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
it("should catch errors thrown inside of stateless component function", function() {
// Given
// eslint-disable-next-line react/require-render-return
const BrokenComponent = () => { throw new Error("This component is broken") }
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
const Component = system.getSystem().getComponent("BrokenComponent")
const wrapper = mount(<Component />)
expect(wrapper.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
it("should catch errors thrown inside of container created from class component", function() {
// Given
class BrokenComponent extends React.Component {
render() {
throw new Error("This component is broken")
}
}
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
const Component = system.getSystem().getComponent("BrokenComponent", true)
const wrapper = render(
<Provider store={system.getStore()}>
<Component />
</Provider>
)
// Then
expect(wrapper.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
it("should catch errors thrown inside of container created from stateless component function", function() {
// Given
const BrokenComponent = () => { throw new Error("This component is broken") }
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
const Component = system.getSystem().getComponent("BrokenComponent", true)
const wrapper = mount(
<Provider store={system.getStore()}>
<Component />
</Provider>
)
// Then
expect(wrapper.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
})
})
})