From ad4673cb363e7272955d4cbd9550f44a3e38e74c Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Mon, 8 Nov 2021 10:30:36 +0100 Subject: [PATCH] [IMP] component: re-introduce error handling in lifecycle --- src/component/component_node.ts | 36 +- src/component/error_handling.ts | 50 ++ src/component/fibers.ts | 6 +- src/component/lifecycle_hooks.ts | 13 + src/component/scheduler.ts | 18 +- src/index.ts | 1 + .../__snapshots__/error_handling.test.ts.snap | 524 ++++++++++++++++++ tests/components/error_handling.test.ts | 85 ++- 8 files changed, 668 insertions(+), 65 deletions(-) create mode 100644 src/component/error_handling.ts diff --git a/src/component/component_node.ts b/src/component/component_node.ts index aa8e672c..370cb2e0 100644 --- a/src/component/component_node.ts +++ b/src/component/component_node.ts @@ -10,6 +10,7 @@ import { RootFiber, __internal__destroyed, } from "./fibers"; +import { handleError, fibersInError } from "./error_handling"; import { applyDefaultProps } from "./props_validation"; import { STATUS } from "./status"; import { applyStyles } from "./style"; @@ -41,7 +42,15 @@ export function component( node.updateAndRender(props, parentFiber); } else { // new component - const C = isDynamic ? name : parent.constructor.components[name as any]; + let C; + if (isDynamic) { + C = name; + } else { + C = parent.constructor.components[name as any]; + if (!C) { + throw new Error(`Cannot find the definition of component "${name}"`); + } + } node = new ComponentNode(C, props, ctx.app, ctx); ctx.children[key] = node; @@ -112,23 +121,27 @@ export class ComponentNode implements VNode f.call(component))); - await prom; + try { + await Promise.all(this.willStart.map((f) => f.call(component))); + } catch (e) { + handleError(this, e); + return; + } if (this.status === STATUS.NEW && this.fiber === fiber) { this._render(fiber); } } async render() { - if (this.fiber && !this.fiber.bdom) { - return this.fiber.root.promise; + let fiber = this.fiber; + if (fiber && !fiber.bdom && !fibersInError.has(fiber)) { + return fiber.root.promise; } if (!this.bdom && !this.fiber) { // should find a way to return the future mounting promise return; } - - const fiber = makeRootFiber(this); + fiber = makeRootFiber(this); this.app.scheduler.addFiber(fiber); await Promise.resolve(); if (this.status === STATUS.DESTROYED) { @@ -143,15 +156,10 @@ export class ComponentNode implements VNode = new WeakMap(); +export const nodeErrorHandlers: WeakMap void)[]> = new WeakMap(); + +function _handleError(node: ComponentNode | null, error: Error): boolean { + if (!node) { + return false; + } + const fiber = node.fiber; + if (fiber) { + fibersInError.set(fiber, error); + } + + const errorHandlers = nodeErrorHandlers.get(node); + if (errorHandlers) { + if (fiber && !fiber.children.length) { + fiber.root.counter--; + } + + let propagate = true; + for (const h of errorHandlers) { + try { + h(error); + propagate = false; + } catch (e) { + error = e as Error; + } + } + + if (propagate) { + return _handleError(node.parent, error); + } + return true; + } else { + return _handleError(node.parent, error); + } +} + +export function handleError(node: ComponentNode, error: Error) { + const fiber = node.fiber!; + fibersInError.set(fiber.root, error); + + if (!_handleError(node, error)) { + try { + node.app.destroy(); + } catch (e) {} + } +} diff --git a/src/component/fibers.ts b/src/component/fibers.ts index 71912945..bb0c9ceb 100644 --- a/src/component/fibers.ts +++ b/src/component/fibers.ts @@ -2,6 +2,7 @@ import type { BDom } from "../blockdom"; import { mount } from "../blockdom"; import type { ComponentNode } from "./component_node"; import { STATUS } from "./status"; +import { fibersInError } from "./error_handling"; // import { mountBlock } from "./bdom/block"; export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber { @@ -27,6 +28,10 @@ export function makeRootFiber(node: ComponentNode): Fiber { current.children = []; root.counter++; current.bdom = null; + if (fibersInError.has(current)) { + fibersInError.delete(current); + fibersInError.delete(root); + } return current; } const fiber = new RootFiber(node); @@ -81,7 +86,6 @@ export class Fiber { export class RootFiber extends Fiber { counter: number = 1; - error: Error | null = null; resolve: any; promise: Promise; reject: any; diff --git a/src/component/lifecycle_hooks.ts b/src/component/lifecycle_hooks.ts index fbd9adff..f493ca8b 100644 --- a/src/component/lifecycle_hooks.ts +++ b/src/component/lifecycle_hooks.ts @@ -1,4 +1,5 @@ import { getCurrent } from "./component_node"; +import { nodeErrorHandlers } from "./error_handling"; // ----------------------------------------------------------------------------- // hooks @@ -47,3 +48,15 @@ export function onRender(fn: () => void | any) { return renderFn(); }; } + +export function onError(fn: (error: Error) => void | any) { + const node = getCurrent()!; + let handlers = nodeErrorHandlers.get(node); + if (handlers) { + handlers.push(fn); + } else { + handlers = []; + handlers.push(fn); + nodeErrorHandlers.set(node, handlers); + } +} diff --git a/src/component/scheduler.ts b/src/component/scheduler.ts index 0f32c79f..36130364 100644 --- a/src/component/scheduler.ts +++ b/src/component/scheduler.ts @@ -1,4 +1,5 @@ import { Fiber, RootFiber } from "./fibers"; +import { handleError, fibersInError } from "./error_handling"; import { STATUS } from "./status"; // ----------------------------------------------------------------------------- @@ -46,20 +47,27 @@ export class Scheduler { this.tasks.delete(fiber); return; } - if (fiber.error) { + const hasError = fibersInError.has(fiber); + if (hasError && fiber.counter !== 0) { this.tasks.delete(fiber); - fiber.reject(fiber.error); + fiber.reject(fibersInError.get(fiber)); return; } if (fiber.node.status === STATUS.DESTROYED) { this.tasks.delete(fiber); return; } + if (fiber.counter === 0) { - if (!fiber.error) { - fiber.complete(); + if (!hasError) { + try { + fiber.complete(); + fiber.resolve(); + } catch (e) { + handleError(fiber.node, e); + fiber.reject(e); + } } - fiber.resolve(); this.tasks.delete(fiber); } }); diff --git a/src/index.ts b/src/index.ts index 17c73b43..6864d328 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,6 +68,7 @@ export { onPatched, onRender, onDestroyed, + onError, } from "./component/lifecycle_hooks"; export const __info__ = {}; diff --git a/tests/components/__snapshots__/error_handling.test.ts.snap b/tests/components/__snapshots__/error_handling.test.ts.snap index 5729c2ac..5721979d 100644 --- a/tests/components/__snapshots__/error_handling.test.ts.snap +++ b/tests/components/__snapshots__/error_handling.test.ts.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`basics display a nice error if it cannot find component 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + return function template(ctx, node, key = \\"\\") { + return component(\`SomeMispelledComponent\`, {}, key + \`__1\`, node, ctx); + } +}" +`; + exports[`basics no component catching error lead to full app destruction 1`] = ` "function anonymous(bdom, helpers ) { @@ -29,3 +41,515 @@ exports[`basics no component catching error lead to full app destruction 2`] = ` } }" `; + +exports[`basics simple catchError 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['a'].b.c; + return block1([d1]); + } +}" +`; + +exports[`basics simple catchError 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2,b3; + if (ctx['error']) { + b2 = text(\`Error\`); + } else { + b3 = component(\`Boom\`, {}, key + \`__1\`, node, ctx); + } + return block1([], [b2, b3]); + } +}" +`; + +exports[`can catch errors can catch an error in a component render function 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
hey
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].flag&&ctx['state'].this.will.crash; + return block1([d1]); + } +}" +`; + +exports[`can catch errors can catch an error in a component render function 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2,b3; + if (ctx['state'].error) { + b2 = text(\`Error handled\`); + } else { + b3 = callSlot(ctx, node, key, 'default'); + } + return block1([], [b2, b3]); + } +}" +`; + +exports[`can catch errors can catch an error in a component render function 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + let assign = Object.assign; + + let block1 = createBlock(\`
\`); + + const slot2 = ctx => (node, key) => { + return component(\`ErrorComponent\`, {flag: ctx['state'].flag}, key + \`__3\`, node, ctx); + } + + return function template(ctx, node, key = \\"\\") { + let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); + return block1([], [b3]); + } +}" +`; + +exports[`can catch errors can catch an error in the constructor call of a component render function 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
Some text
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`can catch errors can catch an error in the constructor call of a component render function 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2,b3; + if (ctx['state'].error) { + b2 = text(\`Error handled\`); + } else { + b3 = callSlot(ctx, node, key, 'default'); + } + return block1([], [b2, b3]); + } +}" +`; + +exports[`can catch errors can catch an error in the constructor call of a component render function 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + let assign = Object.assign; + + let block1 = createBlock(\`
\`); + + const slot2 = ctx => (node, key) => { + return component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx); + } + + return function template(ctx, node, key = \\"\\") { + let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); + return block1([], [b3]); + } +}" +`; + +exports[`can catch errors can catch an error in the initial call of a component render function (parent mounted) 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
hey
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['state'].this.will.crash; + return block1([d1]); + } +}" +`; + +exports[`can catch errors can catch an error in the initial call of a component render function (parent mounted) 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2,b3; + if (ctx['state'].error) { + b2 = text(\`Error handled\`); + } else { + b3 = callSlot(ctx, node, key, 'default'); + } + return block1([], [b2, b3]); + } +}" +`; + +exports[`can catch errors can catch an error in the initial call of a component render function (parent mounted) 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + let assign = Object.assign; + + let block1 = createBlock(\`
\`); + + const slot2 = ctx => (node, key) => { + return component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx); + } + + return function template(ctx, node, key = \\"\\") { + let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); + return block1([], [b3]); + } +}" +`; + +exports[`can catch errors can catch an error in the initial call of a component render function (parent updated) 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
hey
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['state'].this.will.crash; + return block1([d1]); + } +}" +`; + +exports[`can catch errors can catch an error in the initial call of a component render function (parent updated) 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2,b3; + if (ctx['state'].error) { + b2 = text(\`Error handled\`); + } else { + b3 = callSlot(ctx, node, key, 'default'); + } + return block1([], [b2, b3]); + } +}" +`; + +exports[`can catch errors can catch an error in the initial call of a component render function (parent updated) 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + let assign = Object.assign; + + let block1 = createBlock(\`
\`); + + const slot2 = ctx => (node, key) => { + return component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx); + } + + return function template(ctx, node, key = \\"\\") { + let b3; + if (ctx['state'].flag) { + b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); + } + return block1([], [b3]); + } +}" +`; + +exports[`can catch errors can catch an error in the willStart call 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
Some text
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`can catch errors can catch an error in the willStart call 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2,b3; + if (ctx['state'].error) { + b2 = text(\`Error handled\`); + } else { + b3 = callSlot(ctx, node, key, 'default'); + } + return block1([], [b2, b3]); + } +}" +`; + +exports[`can catch errors can catch an error in the willStart call 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + let assign = Object.assign; + + let block1 = createBlock(\`
\`); + + const slot2 = ctx => (node, key) => { + return component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx); + } + + return function template(ctx, node, key = \\"\\") { + let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); + return block1([], [b3]); + } +}" +`; + +exports[`can catch errors catchError in catchError 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['a'].b.c; + return block1([d1]); + } +}" +`; + +exports[`can catch errors catchError in catchError 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2 = component(\`Boom\`, {}, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`can catch errors catchError in catchError 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2,b3; + if (ctx['error']) { + b2 = text(\`Error\`); + } else { + b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx); + } + return block1([], [b2, b3]); + } +}" +`; + +exports[`errors and promises a rendering error in a sub component will reject the mount promise 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = this.will.crash; + return block1([d1]); + } +}" +`; + +exports[`errors and promises a rendering error in a sub component will reject the mount promise 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`errors and promises a rendering error will reject the mount promise 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = this.will.crash; + return block1([d1]); + } +}" +`; + +exports[`errors and promises a rendering error will reject the render promise (with sub components) 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`errors and promises a rendering error will reject the render promise (with sub components) 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx); + let d1 = ctx['x'].y; + return block1([d1], [b2]); + } +}" +`; + +exports[`errors and promises a rendering error will reject the render promise 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2; + if (ctx['flag']) { + b2 = text(this.will.crash); + } + return block1([], [b2]); + } +}" +`; + +exports[`errors and promises an error in mounted call will reject the mount promise 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
abc
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`errors and promises an error in patched call will reject the render promise 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['val']; + return block1([d1]); + } +}" +`; + +exports[`errors and promises an error in willPatch call will reject the render promise 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['val']; + return block1([d1]); + } +}" +`; + +exports[`errors and promises errors in mounted and in willUnmount 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; diff --git a/tests/components/error_handling.test.ts b/tests/components/error_handling.test.ts index 963f5676..f42da78c 100644 --- a/tests/components/error_handling.test.ts +++ b/tests/components/error_handling.test.ts @@ -9,6 +9,7 @@ import { onWillStart, onWillUnmount, useState, + onError, } from "../../src/index"; let fixture: HTMLElement; @@ -48,7 +49,7 @@ describe("basics", () => { expect(error.message).toMatch(regexp); }); - test.skip("display a nice error if it cannot find component", async () => { + test("display a nice error if it cannot find component", async () => { const consoleError = console.error; console.error = jest.fn(); @@ -64,12 +65,12 @@ describe("basics", () => { error = e; } expect(error).toBeDefined(); - expect(error.message).toBe('Cannot find the definition of component "SomeMispelledWidget"'); + expect(error.message).toBe('Cannot find the definition of component "SomeMispelledComponent"'); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); - test.skip("simple catchError", async () => { + test("simple catchError", async () => { class Boom extends Component { static template = xml`
`; } @@ -84,20 +85,21 @@ describe("basics", () => {
`; static components = { Boom }; - error = false; + error: any = false; - // catchError(error) { - // this.error = error; - // this.render(); - // } + setup() { + onError((err) => { + this.error = err; + this.render(); + }); + } } - await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
Error
"); }); }); -describe.skip("errors and promises", () => { +describe("errors and promises", () => { test("a rendering error will reject the mount promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); @@ -283,9 +285,8 @@ describe.skip("errors and promises", () => { expect(error.message).toMatch(regexp); }); - // LPE: relevant: dead code....? test("errors in mounted and in willUnmount", async () => { - expect.assertions(1); + expect.assertions(2); // apparently this expect count in assertions... class Example extends Component { static template = xml`
`; val: any; @@ -309,12 +310,10 @@ describe.skip("errors and promises", () => { }); }); -describe.skip("can catch errors", () => { +describe("can catch errors", () => { test("can catch an error in a component render function", async () => { const consoleError = console.error; console.error = jest.fn(); - const handler = jest.fn(); - //env.qweb.on("error", null, handler); class ErrorComponent extends Component { static template = xml`
hey
`; } @@ -326,8 +325,8 @@ describe.skip("can catch errors", () => {
`; state = useState({ error: false }); - catchError() { - this.state.error = true; + setup() { + onError(() => (this.state.error = true)); } } class App extends Component { @@ -346,12 +345,9 @@ describe.skip("can catch errors", () => { expect(console.error).toBeCalledTimes(0); console.error = consoleError; - expect(handler).toBeCalledTimes(1); }); test("can catch an error in the initial call of a component render function (parent mounted)", async () => { - const handler = jest.fn(); - //env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { @@ -365,8 +361,10 @@ describe.skip("can catch errors", () => {
`; state = useState({ error: false }); - catchError() { - this.state.error = true; + setup() { + onError(() => { + this.state.error = true; + }); } } class App extends Component { @@ -381,12 +379,9 @@ describe.skip("can catch errors", () => { expect(console.error).toBeCalledTimes(0); console.error = consoleError; - expect(handler).toBeCalledTimes(1); }); test("can catch an error in the initial call of a component render function (parent updated)", async () => { - const handler = jest.fn(); - //env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { @@ -400,8 +395,8 @@ describe.skip("can catch errors", () => { `; state = useState({ error: false }); - catchError() { - this.state.error = true; + setup() { + onError(() => (this.state.error = true)); } } class App extends Component { @@ -419,12 +414,9 @@ describe.skip("can catch errors", () => { expect(console.error).toBeCalledTimes(0); console.error = consoleError; - expect(handler).toBeCalledTimes(1); }); test("can catch an error in the constructor call of a component render function", async () => { - const handler = jest.fn(); - //env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); @@ -441,12 +433,12 @@ describe.skip("can catch errors", () => { `; state = useState({ error: false }); - catchError() { - this.state.error = true; + setup() { + onError(() => (this.state.error = true)); } } class App extends Component { - static template = xml` + static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; @@ -456,14 +448,13 @@ describe.skip("can catch errors", () => { expect(console.error).toBeCalledTimes(0); console.error = consoleError; - expect(handler).toBeCalledTimes(1); }); test("can catch an error in the willStart call", async () => { const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { - static template = xml`
Some text
`; + static template = xml`
Some text
`; setup() { onWillStart(async () => { // we wait a little bit to be in a different stack frame @@ -480,8 +471,8 @@ describe.skip("can catch errors", () => { `; state = useState({ error: false }); - catchError() { - this.state.error = true; + setup() { + onError(() => (this.state.error = true)); } } class App extends Component { @@ -587,9 +578,11 @@ describe.skip("can catch errors", () => { `; static components = { Boom }; - // catchError(error) { - // throw error; - // } + setup() { + onError((error) => { + throw error; + }); + } } class Parent extends Component { @@ -602,12 +595,14 @@ describe.skip("can catch errors", () => { `; static components = { Child }; - error = false; + error: any = false; - // catchError(error) { - // this.error = error; - // this.render(); - // } + setup() { + onError((error) => { + this.error = error; + this.render(); + }); + } } await mount(Parent, fixture);