diff --git a/src/component/error_handling.ts b/src/component/error_handling.ts index dca56dd9..e390db2b 100644 --- a/src/component/error_handling.ts +++ b/src/component/error_handling.ts @@ -1,5 +1,5 @@ import type { ComponentNode } from "./component_node"; -import type { Fiber } from "./fibers"; +import { Fiber } from "./fibers"; export const fibersInError: WeakMap = new WeakMap(); export const nodeErrorHandlers: WeakMap void)[]> = new WeakMap(); @@ -38,13 +38,23 @@ function _handleError(node: ComponentNode | null, error: Error, isFirstRound = f } } -export function handleError(node: ComponentNode, error: Error) { - const fiber = node.fiber!; +export function handleError(entity: ComponentNode | Fiber, error: Error) { + let node: ComponentNode; + let fiber: Fiber; + if (entity instanceof Fiber) { + fiber = entity; + node = entity.node; + } else { + node = entity; + fiber = entity.fiber!; + } fibersInError.set(fiber.root, error); - if (!_handleError(node, error, true)) { + const handled = _handleError(node, error, true); + if (!handled) { try { node.app.destroy(); } catch (e) {} } + return handled; } diff --git a/src/component/fibers.ts b/src/component/fibers.ts index 39d3b204..32df804d 100644 --- a/src/component/fibers.ts +++ b/src/component/fibers.ts @@ -2,7 +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 { fibersInError, handleError } from "./error_handling"; export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber { let current = node.fiber; @@ -106,56 +106,63 @@ export class RootFiber extends Fiber { complete() { const node = this.node; - - // Step 1: calling all willPatch lifecycle hooks - for (let fiber of this.willPatch) { - // because of the asynchronous nature of the rendering, some parts of the - // UI may have been rendered, then deleted in a followup rendering, and we - // do not want to call onWillPatch in that case. - let node = fiber.node; - if (node.fiber === fiber) { - const component = node.component; - for (let cb of node.willPatch) { - cb.call(component); + let current: Fiber | undefined = undefined; + try { + // Step 1: calling all willPatch lifecycle hooks + for (current of this.willPatch) { + // because of the asynchronous nature of the rendering, some parts of the + // UI may have been rendered, then deleted in a followup rendering, and we + // do not want to call onWillPatch in that case. + let node = current.node; + if (node.fiber === current) { + const component = node.component; + for (let cb of node.willPatch) { + cb.call(component); + } } } - } + current = undefined; - // Step 2: patching the dom - node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0); - this.appliedToDom = true; + // Step 2: patching the dom + node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0); + this.appliedToDom = true; - // Step 3: calling all destroyed hooks - for (let node of __internal__destroyed) { - for (let cb of node.destroyed) { - cb(); - } - } - __internal__destroyed.length = 0; - - // Step 4: calling all mounted lifecycle hooks - let current; - let mountedFibers = this.mounted; - while ((current = mountedFibers.pop())) { - if (current.appliedToDom) { - for (let cb of current.node.mounted) { + // Step 3: calling all destroyed hooks + for (let node of __internal__destroyed) { + for (let cb of node.destroyed) { cb(); } } - } + __internal__destroyed.length = 0; - // Step 5: calling all patched hooks - let patchedFibers = this.patched; - while ((current = patchedFibers.pop())) { - if (current.appliedToDom) { - for (let cb of current.node.patched) { - cb(); + // Step 4: calling all mounted lifecycle hooks + let mountedFibers = this.mounted; + while ((current = mountedFibers.pop())) { + current = current; + if (current.appliedToDom) { + for (let cb of current.node.mounted) { + cb(); + } } } - } - // unregistering the fiber - node.fiber = null; + // Step 5: calling all patched hooks + let patchedFibers = this.patched; + while ((current = patchedFibers.pop())) { + current = current; + if (current.appliedToDom) { + for (let cb of current.node.patched) { + cb(); + } + } + } + // unregistering the fiber + node.fiber = null; + } catch (e) { + if (!handleError(current || this, e)) { + this.reject(e); + } + } } } @@ -177,25 +184,31 @@ export class MountFiber extends RootFiber { this.position = options.position || "last-child"; } complete() { - const node = this.node; - node.bdom = this.bdom; - if (this.position === "last-child" || this.target.childNodes.length === 0) { - mount(node.bdom!, this.target); - } else { - const firstChild = this.target.childNodes[0]; - mount(node.bdom!, this.target, firstChild); - } - node.status = STATUS.MOUNTED; - this.appliedToDom = true; - let current; - let mountedFibers = this.mounted; - while ((current = mountedFibers.pop())) { - if (current.appliedToDom) { - for (let cb of current.node.mounted) { - cb(); + let current: Fiber | undefined = this; + try { + const node = this.node; + node.bdom = this.bdom; + if (this.position === "last-child" || this.target.childNodes.length === 0) { + mount(node.bdom!, this.target); + } else { + const firstChild = this.target.childNodes[0]; + mount(node.bdom!, this.target, firstChild); + } + node.status = STATUS.MOUNTED; + this.appliedToDom = true; + let mountedFibers = this.mounted; + while ((current = mountedFibers.pop())) { + if (current.appliedToDom) { + for (let cb of current.node.mounted) { + cb(); + } } } + node.fiber = null; + } catch (e) { + if (!handleError(current as Fiber, e)) { + this.reject(e); + } } - node.fiber = null; } } diff --git a/src/component/scheduler.ts b/src/component/scheduler.ts index 36130364..ae24f828 100644 --- a/src/component/scheduler.ts +++ b/src/component/scheduler.ts @@ -1,5 +1,5 @@ import { Fiber, RootFiber } from "./fibers"; -import { handleError, fibersInError } from "./error_handling"; +import { fibersInError } from "./error_handling"; import { STATUS } from "./status"; // ----------------------------------------------------------------------------- @@ -60,13 +60,8 @@ export class Scheduler { if (fiber.counter === 0) { if (!hasError) { - try { - fiber.complete(); - fiber.resolve(); - } catch (e) { - handleError(fiber.node, e); - fiber.reject(e); - } + fiber.complete(); + fiber.resolve(); } this.tasks.delete(fiber); } diff --git a/tests/components/__snapshots__/error_handling.test.ts.snap b/tests/components/__snapshots__/error_handling.test.ts.snap index 3ae1771a..30916e66 100644 --- a/tests/components/__snapshots__/error_handling.test.ts.snap +++ b/tests/components/__snapshots__/error_handling.test.ts.snap @@ -369,6 +369,116 @@ exports[`can catch errors can catch an error in the initial call of a component }" `; +exports[`can catch errors can catch an error in the mounted 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, safeOutput } = helpers; + + let block1 = createBlock(\`
Some text
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + +exports[`can catch errors can catch an error in the mounted 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, safeOutput } = 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 mounted 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, safeOutput } = 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 willPatch 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, safeOutput } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].message; + return block1([d1]); + } +}" +`; + +exports[`can catch errors can catch an error in the willPatch 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, safeOutput } = 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 willPatch 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, safeOutput } = helpers; + let assign = Object.assign; + + let block1 = createBlock(\`
\`); + + const slot2 = ctx => (node, key) => { + return component(\`ErrorComponent\`, {message: ctx['state'].message}, key + \`__3\`, node, ctx); + } + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['state'].message; + let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); + return block1([d1], [b3]); + } +}" +`; + exports[`can catch errors can catch an error in the willStart call 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/error_handling.test.ts b/tests/components/error_handling.test.ts index edef5ffc..5f24d5e9 100644 --- a/tests/components/error_handling.test.ts +++ b/tests/components/error_handling.test.ts @@ -564,33 +564,30 @@ describe("can catch errors", () => { console.error = consoleError; }); - test.skip("can catch an error in the mounted call", async () => { - // we do not catch error in mounted anymore - console.error = jest.fn(); - // env.qweb.addTemplates(` - // - //
- // Error handled - // - //
- //
Some text
- //
- // - //
- //
`); + test("can catch an error in the mounted call", async () => { class ErrorComponent extends Component { - mounted() { - throw new Error("NOOOOO"); + static template = xml`
Some text
`; + setup() { + onMounted(() => { + throw new Error("NOOOOO"); + }); } } class ErrorBoundary extends Component { + static template = xml`
+ Error handled + +
`; state = useState({ error: false }); - catchError() { - this.state.error = true; + setup() { + onError(() => (this.state.error = true)); } } class App extends Component { + static template = xml`
+ +
`; static components = { ErrorBoundary, ErrorComponent }; } await mount(App, fixture); @@ -600,10 +597,7 @@ describe("can catch errors", () => { expect(fixture.innerHTML).toBe("
Error handled
"); }); - test.skip("can catch an error in the willPatch call", async () => { - // we do not catch error in willPatch anymore - const consoleError = console.error; - console.error = jest.fn(); + test("can catch an error in the willPatch call", async () => { class ErrorComponent extends Component { static template = xml`
`; setup() { @@ -620,8 +614,8 @@ describe("can catch errors", () => { `; state = useState({ error: false }); - catchError() { - this.state.error = true; + setup() { + onError(() => (this.state.error = true)); } } class App extends Component { @@ -640,8 +634,6 @@ describe("can catch errors", () => { await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
def
Error handled
"); - expect(console.error).toHaveBeenCalledTimes(1); - console.error = consoleError; }); test("catchError in catchError", async () => {