Add try/catch protection for statePlugin interfaces
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user