Add try/catch protection for statePlugin interfaces

This commit is contained in:
Kyle Shockey
2018-01-03 13:56:29 -06:00
parent b749ea737d
commit c601f9b0ca
2 changed files with 323 additions and 116 deletions

View File

@@ -176,7 +176,7 @@ export default class Store {
if(!isFn(newAction)) { if(!isFn(newAction)) {
throw new TypeError("wrapActions needs to return a function that returns a new function (ie the wrapped action)") throw new TypeError("wrapActions needs to return a function that returns a new function (ie the wrapped action)")
} }
return newAction return wrapWithTryCatch(newAction)
}, action || Function.prototype) }, action || Function.prototype)
}) })
} }
@@ -256,11 +256,11 @@ export default class Store {
return objMap(obj, (fn) => { return objMap(obj, (fn) => {
return (...args) => { return (...args) => {
let res = fn.apply(null, [getNestedState(), ...args]) let res = wrapWithTryCatch(fn).apply(null, [getNestedState(), ...args])
// If a selector returns a function, give it the system - for advanced usage // If a selector returns a function, give it the system - for advanced usage
if(typeof(res) === "function") if(typeof(res) === "function")
res = res(getSystem()) res = wrapWithTryCatch(res)(getSystem())
return res return res
} }
@@ -331,7 +331,7 @@ function callAfterLoad(plugins, system, { hasLoaded } = {}) {
if(isObject(plugins) && !isArray(plugins)) { if(isObject(plugins) && !isArray(plugins)) {
if(typeof plugins.afterLoad === "function") { if(typeof plugins.afterLoad === "function") {
calledSomething = true calledSomething = true
plugins.afterLoad.call(this, system) wrapWithTryCatch(plugins.afterLoad).call(this, system)
} }
} }
@@ -436,14 +436,36 @@ function makeReducer(reducerObj) {
if(!reducerObj) if(!reducerObj)
return state return state
let redFn = reducerObj[action.type] let redFn = (reducerObj[action.type])
if(redFn) { if(redFn) {
return redFn(state, action) const res = wrapWithTryCatch(redFn)(state, action)
// If the try/catch wrapper kicks in, we'll get null back...
// in that case, we want to avoid making any changes to state
return res === null ? state : res
} }
return state return state
} }
} }
function wrapWithTryCatch(fn, {
logErrors = true
} = {}) {
if(typeof fn !== "function") {
return fn
}
return function(...args) {
try {
return fn.call(this, ...args)
} catch(e) {
if(logErrors) {
console.error(e)
}
return null
}
}
}
function configureStore(rootReducer, initialState, getSystem) { function configureStore(rootReducer, initialState, getSystem) {
const store = createStoreWithMiddleware(rootReducer, initialState, getSystem) const store = createStoreWithMiddleware(rootReducer, initialState, getSystem)

View File

@@ -571,116 +571,6 @@ describe("bound system", function(){
// Then // Then
expect(renderedComponent.text()).toEqual("This came from mapStateToProps and this came from the system and this came from my own props") expect(renderedComponent.text()).toEqual("This came from mapStateToProps and this came from the system and this came from my own props")
}) })
it("should catch errors thrown inside of React Component Class render methods", function() {
// Given
// eslint-disable-next-line react/require-render-return
class BrokenComponent extends React.Component {
render() {
throw new Error("This component is broken")
}
}
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
var 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 render methods", function() {
// Given
// eslint-disable-next-line react/require-render-return
class BrokenComponent extends PureComponent {
render() {
throw new Error("This component is broken")
}
}
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
var 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 stateless component functions", function() {
// Given
// eslint-disable-next-line react/require-render-return
let BrokenComponent = function BrokenComponent() { throw new Error("This component is broken") }
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
var Component = system.getSystem().getComponent("BrokenComponent")
const renderedComponent = render(<Component />)
// Then
expect(renderedComponent.text().startsWith("😱 Could not render")).toEqual(true)
})
it("should catch errors thrown inside of container components", function() {
// Given
// eslint-disable-next-line react/require-render-return
class BrokenComponent extends React.Component {
render() {
throw new Error("This component is broken")
}
}
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
var Component = system.getSystem().getComponent("BrokenComponent", true)
const renderedComponent = render(
<Provider store={system.getStore()}>
<Component />
</Provider>
)
// Then
expect(renderedComponent.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
}) })
describe("afterLoad", function() { describe("afterLoad", function() {
@@ -793,4 +683,299 @@ describe("bound system", function(){
expect(res).toEqual("so selective") expect(res).toEqual("so selective")
}) })
}) })
describe("error catching", function() {
it("should encapsulate thrown errors in an afterLoad method", function() {
// Given
const ThrowyPlugin = {
afterLoad(system) {
throw new Error("afterLoad BREAKS STUFF!")
},
statePlugins: {
doge: {
selectors: {
wow: () => (system) => {
return "so selective"
}
}
}
}
}
const system = new System({
plugins: []
})
// When
expect(function() {
system.register([ThrowyPlugin])
// var resSystem = system.getSystem()
}).toNotThrow()
})
it("should encapsulate thrown errors in an action creator", function(){
// Given
const system = new System({
plugins: {
statePlugins: {
throw: {
actions: {
func() {
throw new Error("this action creator THROWS!")
}
}
}
}
}
})
expect(function() {
// TODO: fix existing action error catcher that creates THROWN ERR actions
system.getSystem().throwActions.func()
}).toNotThrow()
})
it("should encapsulate thrown errors in a reducer", function(){
// Given
const system = new System({
plugins: {
statePlugins: {
throw: {
actions: {
func: () => {
return {
type: "THROW_FUNC",
payload: "BOOM!"
}
}
},
reducers: {
"THROW_FUNC": (state, action) => {
throw new Error("this reducer EXPLODES!")
}
}
}
}
}
})
expect(function() {
system.getSystem().throwActions.func()
}).toNotThrow()
})
it("should encapsulate thrown errors in a selector", function(){
// Given
const system = new System({
plugins: {
statePlugins: {
throw: {
selectors: {
func: (state, arg1) => {
throw new Error("this selector THROWS!")
}
}
}
}
}
})
expect(system.getSystem().throwSelectors.func).toNotThrow()
})
it("should encapsulate thrown errors in a complex selector", function(){
// Given
const system = new System({
plugins: {
statePlugins: {
throw: {
selectors: {
func: (state, arg1) => system => {
throw new Error("this selector THROWS!")
}
}
}
}
}
})
expect(system.getSystem().throwSelectors.func).toNotThrow()
})
it("should encapsulate thrown errors in a wrapAction", function(){
// Given
const system = new System({
plugins: {
statePlugins: {
throw: {
actions: {
func: () => {
return {
type: "THROW_FUNC",
payload: "this original action does NOT throw"
}
}
},
wrapActions: {
func: (ori) => (...args) => {
throw new Error("this wrapAction UNRAVELS EVERYTHING!")
}
}
}
}
}
})
expect(system.getSystem().throwActions.func).toNotThrow()
})
it("should encapsulate thrown errors in a wrapSelector", function(){
// Given
const system = new System({
plugins: {
statePlugins: {
throw: {
selectors: {
func: (state, arg1) => {
return 123
}
},
wrapSelectors: {
func: (ori) => (...props) => {
return ori(...props)
}
}
}
}
}
})
expect(system.getSystem().throwSelectors.func).toNotThrow()
})
describe("components", function() {
it("should catch errors thrown inside of React Component Class render methods", function() {
// Given
// eslint-disable-next-line react/require-render-return
class BrokenComponent extends React.Component {
render() {
throw new Error("This component is broken")
}
}
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
var 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 render methods", function() {
// Given
// eslint-disable-next-line react/require-render-return
class BrokenComponent extends PureComponent {
render() {
throw new Error("This component is broken")
}
}
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
var 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 stateless component functions", function() {
// Given
// eslint-disable-next-line react/require-render-return
let BrokenComponent = function BrokenComponent() { throw new Error("This component is broken") }
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
var Component = system.getSystem().getComponent("BrokenComponent")
const renderedComponent = render(<Component />)
// Then
expect(renderedComponent.text().startsWith("😱 Could not render")).toEqual(true)
})
it("should catch errors thrown inside of container components", function() {
// Given
// eslint-disable-next-line react/require-render-return
class BrokenComponent extends React.Component {
render() {
throw new Error("This component is broken")
}
}
const system = new System({
plugins: [
ViewPlugin,
{
components: {
BrokenComponent
}
}
]
})
// When
var Component = system.getSystem().getComponent("BrokenComponent", true)
const renderedComponent = render(
<Provider store={system.getStore()}>
<Component />
</Provider>
)
// Then
expect(renderedComponent.text()).toEqual("😱 Could not render BrokenComponent, see the console.")
})
})
})
}) })