feat(error-handling): introduce unified and configurable error handling (#7761)
Refs #7778
This commit is contained in:
@@ -7,7 +7,6 @@ module.exports = {
|
|||||||
'**/test/unit/*.js?(x)',
|
'**/test/unit/*.js?(x)',
|
||||||
'**/test/unit/**/*.js?(x)',
|
'**/test/unit/**/*.js?(x)',
|
||||||
],
|
],
|
||||||
// testMatch: ['**/test/unit/core/plugins/auth/actions.js'],
|
|
||||||
setupFilesAfterEnv: ['<rootDir>/test/unit/setup.js'],
|
setupFilesAfterEnv: ['<rootDir>/test/unit/setup.js'],
|
||||||
testPathIgnorePatterns: [
|
testPathIgnorePatterns: [
|
||||||
'<rootDir>/node_modules/',
|
'<rootDir>/node_modules/',
|
||||||
|
|||||||
@@ -233,3 +233,168 @@ const ui = SwaggerUIBundle({
|
|||||||
...snippetConfig,
|
...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`.
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export default class BaseLayout extends React.Component {
|
|||||||
const SchemesContainer = getComponent("SchemesContainer", true)
|
const SchemesContainer = getComponent("SchemesContainer", true)
|
||||||
const AuthorizeBtnContainer = getComponent("AuthorizeBtnContainer", true)
|
const AuthorizeBtnContainer = getComponent("AuthorizeBtnContainer", true)
|
||||||
const FilterContainer = getComponent("FilterContainer", true)
|
const FilterContainer = getComponent("FilterContainer", true)
|
||||||
const ErrorBoundary = getComponent("ErrorBoundary", true)
|
|
||||||
let isSwagger2 = specSelectors.isSwagger2()
|
let isSwagger2 = specSelectors.isSwagger2()
|
||||||
let isOAS3 = specSelectors.isOAS3()
|
let isOAS3 = specSelectors.isOAS3()
|
||||||
|
|
||||||
@@ -87,7 +86,6 @@ export default class BaseLayout extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='swagger-ui'>
|
<div className='swagger-ui'>
|
||||||
<ErrorBoundary targetName="BaseLayout">
|
|
||||||
<SvgAssets />
|
<SvgAssets />
|
||||||
<VersionPragmaFilter isSwagger2={isSwagger2} isOAS3={isOAS3} alsoShow={<Errors/>}>
|
<VersionPragmaFilter isSwagger2={isSwagger2} isOAS3={isOAS3} alsoShow={<Errors/>}>
|
||||||
<Errors/>
|
<Errors/>
|
||||||
@@ -120,7 +118,6 @@ export default class BaseLayout extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</VersionPragmaFilter>
|
</VersionPragmaFilter>
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { pascalCaseFilename } from "core/utils"
|
import { pascalCaseFilename } from "core/utils"
|
||||||
|
import SafeRender from "core/plugins/safe-render"
|
||||||
|
|
||||||
const request = require.context(".", true, /\.jsx?$/)
|
const request = require.context(".", true, /\.jsx?$/)
|
||||||
|
|
||||||
@@ -18,4 +19,6 @@ request.keys().forEach( function( key ){
|
|||||||
allPlugins[pascalCaseFilename(key)] = mod.default ? mod.default : mod
|
allPlugins[pascalCaseFilename(key)] = mod.default ? mod.default : mod
|
||||||
})
|
})
|
||||||
|
|
||||||
|
allPlugins.SafeRender = SafeRender
|
||||||
|
|
||||||
export default allPlugins
|
export default allPlugins
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import PropTypes from "prop-types"
|
import PropTypes from "prop-types"
|
||||||
import React, { Component } from "react"
|
import React, { Component } from "react"
|
||||||
|
|
||||||
|
import { componentDidCatch } from "../fn"
|
||||||
import Fallback from "./fallback"
|
import Fallback from "./fallback"
|
||||||
|
|
||||||
export class ErrorBoundary extends Component {
|
export class ErrorBoundary extends Component {
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = { hasError: false, error: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error) {
|
static getDerivedStateFromError(error) {
|
||||||
return { hasError: true, error }
|
return { hasError: true, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args)
|
||||||
|
this.state = { hasError: false, error: null }
|
||||||
|
}
|
||||||
|
|
||||||
componentDidCatch(error, errorInfo) {
|
componentDidCatch(error, errorInfo) {
|
||||||
console.error(error, errorInfo) // eslint-disable-line no-console
|
this.props.fn.componentDidCatch(error, errorInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { getComponent, targetName, children } = this.props
|
const { getComponent, targetName, children } = this.props
|
||||||
const FallbackComponent = getComponent("Fallback")
|
|
||||||
|
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
|
const FallbackComponent = getComponent("Fallback")
|
||||||
return <FallbackComponent name={targetName} />
|
return <FallbackComponent name={targetName} />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ export class ErrorBoundary extends Component {
|
|||||||
ErrorBoundary.propTypes = {
|
ErrorBoundary.propTypes = {
|
||||||
targetName: PropTypes.string,
|
targetName: PropTypes.string,
|
||||||
getComponent: PropTypes.func,
|
getComponent: PropTypes.func,
|
||||||
|
fn: PropTypes.object,
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node,
|
PropTypes.node,
|
||||||
@@ -39,6 +41,9 @@ ErrorBoundary.propTypes = {
|
|||||||
ErrorBoundary.defaultProps = {
|
ErrorBoundary.defaultProps = {
|
||||||
targetName: "this component",
|
targetName: "this component",
|
||||||
getComponent: () => Fallback,
|
getComponent: () => Fallback,
|
||||||
|
fn: {
|
||||||
|
componentDidCatch,
|
||||||
|
},
|
||||||
children: null,
|
children: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
32
src/core/plugins/safe-render/fn.jsx
Normal file
32
src/core/plugins/safe-render/fn.jsx
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
42
src/core/plugins/safe-render/index.js
Normal file
42
src/core/plugins/safe-render/index.js
Normal 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
|
||||||
1
src/core/plugins/view/fn.js
Normal file
1
src/core/plugins/view/fn.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const getDisplayName = (WrappedComponent) => WrappedComponent.displayName || WrappedComponent.name || "Component"
|
||||||
@@ -1,26 +1,23 @@
|
|||||||
import * as rootInjects from "./root-injects"
|
|
||||||
import { memoize } from "core/utils"
|
import { memoize } from "core/utils"
|
||||||
|
|
||||||
import ErrorBoundary from "./error-boundary"
|
import { getComponent, render, withMappedContainer } from "./root-injects"
|
||||||
import Fallback from "./fallback"
|
import { getDisplayName } from "./fn"
|
||||||
|
|
||||||
export default function({getComponents, getStore, getSystem}) {
|
|
||||||
|
|
||||||
let { getComponent, render, makeMappedContainer } = rootInjects
|
|
||||||
|
|
||||||
|
const viewPlugin = ({getComponents, getStore, getSystem}) => {
|
||||||
// getComponent should be passed into makeMappedContainer, _already_ memoized... otherwise we have a big performance hit ( think, really big )
|
// 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 memGetComponent = memoize(getComponent(getSystem, getStore, getComponents))
|
||||||
const memMakeMappedContainer = memoize(makeMappedContainer.bind(null, getSystem, getStore, memGetComponent, getComponents))
|
const memMakeMappedContainer = memoize(withMappedContainer(getSystem, getStore, memGetComponent))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rootInjects: {
|
rootInjects: {
|
||||||
getComponent: memGetComponent,
|
getComponent: memGetComponent,
|
||||||
makeMappedContainer: memMakeMappedContainer,
|
makeMappedContainer: memMakeMappedContainer,
|
||||||
render: render.bind(null, getSystem, getStore, getComponent, getComponents),
|
render: render(getSystem, getStore, getComponent, getComponents),
|
||||||
},
|
},
|
||||||
components: {
|
fn: {
|
||||||
ErrorBoundary,
|
getDisplayName,
|
||||||
Fallback,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default viewPlugin
|
||||||
|
|||||||
@@ -1,55 +1,67 @@
|
|||||||
import React, { Component } from "react"
|
import React, { Component } from "react"
|
||||||
import ReactDOM from "react-dom"
|
import ReactDOM from "react-dom"
|
||||||
|
import { compose } from "redux"
|
||||||
import { connect, Provider } from "react-redux"
|
import { connect, Provider } from "react-redux"
|
||||||
import omit from "lodash/omit"
|
import omit from "lodash/omit"
|
||||||
|
import identity from "lodash/identity"
|
||||||
|
|
||||||
const SystemWrapper = (getSystem, ComponentToWrap ) => class extends Component {
|
const withSystem = (getSystem) => (WrappedComponent) => {
|
||||||
|
const { fn } = getSystem()
|
||||||
|
|
||||||
|
class WithSystem extends Component {
|
||||||
render() {
|
render() {
|
||||||
return <ComponentToWrap {...getSystem()} {...this.props} {...this.context} />
|
return <WrappedComponent {...getSystem()} {...this.props} {...this.context} />
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
WithSystem.displayName = `WithSystem(${fn.getDisplayName(WrappedComponent)})`
|
||||||
|
return WithSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
const RootWrapper = (getSystem, reduxStore, ComponentToWrap) => class extends Component {
|
const withRoot = (getSystem, reduxStore) => (WrappedComponent) => {
|
||||||
render() {
|
const { fn } = getSystem()
|
||||||
const { getComponent } = getSystem()
|
|
||||||
const ErrorBoundary = getComponent("ErrorBoundary", true)
|
|
||||||
|
|
||||||
|
class WithRoot extends Component {
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<Provider store={reduxStore}>
|
<Provider store={reduxStore}>
|
||||||
<ErrorBoundary targetName={ComponentToWrap?.name}>
|
<WrappedComponent {...this.props} {...this.context} />
|
||||||
<ComponentToWrap {...this.props} {...this.context} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
WithRoot.displayName = `WithRoot(${fn.getDisplayName(WrappedComponent)})`
|
||||||
|
return WithRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeContainer = (getSystem, component, reduxStore) => {
|
const withConnect = (getSystem, WrappedComponent, reduxStore) => {
|
||||||
const mapStateToProps = function(state, ownProps) {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
const propsForContainerComponent = Object.assign({}, ownProps, getSystem())
|
const props = {...ownProps, ...getSystem()}
|
||||||
const ori = component.prototype.mapStateToProps || (state => { return {state} })
|
const customMapStateToProps = WrappedComponent.prototype?.mapStateToProps || (state => ({state}))
|
||||||
return ori(state, propsForContainerComponent)
|
return customMapStateToProps(state, props)
|
||||||
}
|
}
|
||||||
|
|
||||||
let wrappedWithSystem = SystemWrapper(getSystem, component, reduxStore)
|
return compose(
|
||||||
let connected = connect( mapStateToProps )(wrappedWithSystem)
|
reduxStore ? withRoot(getSystem, reduxStore) : identity,
|
||||||
if(reduxStore)
|
connect(mapStateToProps),
|
||||||
return RootWrapper(getSystem, reduxStore, connected)
|
withSystem(getSystem),
|
||||||
return connected
|
)(WrappedComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleProps = (getSystem, mapping, props, oldProps) => {
|
const handleProps = (getSystem, mapping, props, oldProps) => {
|
||||||
for (let prop in mapping) {
|
for (const prop in mapping) {
|
||||||
let fn = mapping[prop]
|
const fn = mapping[prop]
|
||||||
if(typeof fn === "function")
|
|
||||||
|
if (typeof fn === "function") {
|
||||||
fn(props[prop], oldProps[prop], getSystem())
|
fn(props[prop], oldProps[prop], getSystem())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeMappedContainer = (getSystem, getStore, memGetComponent, getComponents, componentName, mapping) => {
|
export const withMappedContainer = (getSystem, getStore, memGetComponent) => (componentName, mapping) => {
|
||||||
|
const { fn } = getSystem()
|
||||||
return class extends Component {
|
const WrappedComponent = memGetComponent(componentName, "root")
|
||||||
|
|
||||||
|
class WithMappedContainer extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context)
|
super(props, context)
|
||||||
handleProps(getSystem, mapping, props, {})
|
handleProps(getSystem, mapping, props, {})
|
||||||
@@ -60,84 +72,44 @@ export const makeMappedContainer = (getSystem, getStore, memGetComponent, getCom
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let cleanProps = omit(this.props, mapping ? Object.keys(mapping) : [])
|
const cleanProps = omit(this.props, mapping ? Object.keys(mapping) : [])
|
||||||
let Comp = memGetComponent(componentName, "root")
|
return <WrappedComponent {...cleanProps} />
|
||||||
return <Comp {...cleanProps}/>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
WithMappedContainer.displayName = `WithMappedContainer(${fn.getDisplayName(WrappedComponent)})`
|
||||||
|
return WithMappedContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
export const render = (getSystem, getStore, getComponent, getComponents, domNode) => {
|
export const render = (getSystem, getStore, getComponent, getComponents) => (domNode) => {
|
||||||
let App = getComponent(getSystem, getStore, getComponents, "App", "root")
|
const App = getComponent(getSystem, getStore, getComponents)("App", "root")
|
||||||
ReactDOM.render(<App/>, domNode)
|
ReactDOM.render(<App/>, domNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const getComponent = (getSystem, getStore, getComponents) => (componentName, container, config = {}) => {
|
||||||
* 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")
|
|
||||||
|
|
||||||
return (
|
if (typeof componentName !== "string")
|
||||||
<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")
|
|
||||||
throw new TypeError("Need a string, to fetch a component. Was given a " + typeof componentName)
|
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
|
// getComponent has a config object as a third, optional parameter
|
||||||
// using the config object requires the presence of the second parameter, container
|
// using the config object requires the presence of the second parameter, container
|
||||||
// e.g. getComponent("JsonSchema_string_whatever", false, { failSilently: true })
|
// e.g. getComponent("JsonSchema_string_whatever", false, { failSilently: true })
|
||||||
let component = getComponents(componentName)
|
const component = getComponents(componentName)
|
||||||
|
|
||||||
if(!component) {
|
if (!component) {
|
||||||
if (!config.failSilently) {
|
if (!config.failSilently) {
|
||||||
getSystem().log.warn("Could not find component:", componentName)
|
getSystem().log.warn("Could not find component:", componentName)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!container)
|
if(!container) {
|
||||||
return wrapRender(getSystem, component)
|
return component
|
||||||
|
}
|
||||||
|
|
||||||
if(container === "root")
|
if(container === "root") {
|
||||||
return makeContainer(getSystem, component, getStore())
|
return withConnect(getSystem, component, getStore())
|
||||||
|
}
|
||||||
|
|
||||||
// container == truthy
|
// container == truthy
|
||||||
return makeContainer(getSystem, wrapRender(getSystem, component))
|
return withConnect(getSystem, component)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import configsPlugin from "core/plugins/configs"
|
|||||||
import deepLinkingPlugin from "core/plugins/deep-linking"
|
import deepLinkingPlugin from "core/plugins/deep-linking"
|
||||||
import filter from "core/plugins/filter"
|
import filter from "core/plugins/filter"
|
||||||
import onComplete from "core/plugins/on-complete"
|
import onComplete from "core/plugins/on-complete"
|
||||||
|
import safeRender from "core/plugins/safe-render"
|
||||||
|
|
||||||
import OperationContainer from "core/containers/OperationContainer"
|
import OperationContainer from "core/containers/OperationContainer"
|
||||||
|
|
||||||
@@ -193,6 +194,7 @@ export default function() {
|
|||||||
deepLinkingPlugin,
|
deepLinkingPlugin,
|
||||||
filter,
|
filter,
|
||||||
onComplete,
|
onComplete,
|
||||||
requestSnippets
|
requestSnippets,
|
||||||
|
safeRender(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import StandaloneLayout from "./layout"
|
import StandaloneLayout from "./layout"
|
||||||
import TopbarPlugin from "plugins/topbar"
|
import TopbarPlugin from "plugins/topbar"
|
||||||
import ConfigsPlugin from "corePlugins/configs"
|
import ConfigsPlugin from "corePlugins/configs"
|
||||||
|
import SafeRenderPlugin from "core/plugins/safe-render"
|
||||||
|
|
||||||
// the Standalone preset
|
// the Standalone preset
|
||||||
|
|
||||||
@@ -11,5 +12,13 @@ export default [
|
|||||||
return {
|
return {
|
||||||
components: { StandaloneLayout }
|
components: { StandaloneLayout }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
SafeRenderPlugin({
|
||||||
|
fullOverride: true,
|
||||||
|
componentList: [
|
||||||
|
"Topbar",
|
||||||
|
"StandaloneLayout",
|
||||||
|
"onlineValidatorBadge"
|
||||||
|
]
|
||||||
|
})
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,22 +21,16 @@ export default class StandaloneLayout extends React.Component {
|
|||||||
const Topbar = getComponent("Topbar", true)
|
const Topbar = getComponent("Topbar", true)
|
||||||
const BaseLayout = getComponent("BaseLayout", true)
|
const BaseLayout = getComponent("BaseLayout", true)
|
||||||
const OnlineValidatorBadge = getComponent("onlineValidatorBadge", true)
|
const OnlineValidatorBadge = getComponent("onlineValidatorBadge", true)
|
||||||
const ErrorBoundary = getComponent("ErrorBoundary", true)
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className='swagger-ui'>
|
<Container className='swagger-ui'>
|
||||||
<ErrorBoundary targetName="Topbar">
|
|
||||||
{Topbar ? <Topbar /> : null}
|
{Topbar ? <Topbar /> : null}
|
||||||
</ErrorBoundary>
|
|
||||||
<BaseLayout />
|
<BaseLayout />
|
||||||
<ErrorBoundary targetName="OnlineValidatorBadge">
|
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<OnlineValidatorBadge />
|
<OnlineValidatorBadge />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</ErrorBoundary>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
253
test/unit/core/plugins/safe-render/index.jsx
Normal file
253
test/unit/core/plugins/safe-render/index.jsx
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -911,142 +911,5 @@ describe("bound system", function(){
|
|||||||
|
|
||||||
expect(system.getSystem().throwSelectors.func).not.toThrow()
|
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.")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user