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)',
|
||||
],
|
||||
// testMatch: ['**/test/unit/core/plugins/auth/actions.js'],
|
||||
setupFilesAfterEnv: ['<rootDir>/test/unit/setup.js'],
|
||||
testPathIgnorePatterns: [
|
||||
'<rootDir>/node_modules/',
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
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 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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
})
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
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