mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] component, fiber: error_handling at the Fiber level
Before this commit, errors triggered at the level of the fiber (as opposed to at the level of a component's rendering), were handled as the very top level of the rendering, that is, in the scheduler. This was wrong because components below in the rendering tree would not have a chance to handle their children's or their own errors. After this commit, error triggered in willPatch, onMounted and onPatched are correctly handled at the closest component to where they were thrown.
This commit is contained in:
committed by
Aaron Bohy
parent
cb107cef7d
commit
ed3e6dcbb6
@@ -1,5 +1,5 @@
|
|||||||
import type { ComponentNode } from "./component_node";
|
import type { ComponentNode } from "./component_node";
|
||||||
import type { Fiber } from "./fibers";
|
import { Fiber } from "./fibers";
|
||||||
|
|
||||||
export const fibersInError: WeakMap<Fiber, Error> = new WeakMap();
|
export const fibersInError: WeakMap<Fiber, Error> = new WeakMap();
|
||||||
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: Error) => void)[]> = new WeakMap();
|
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: Error) => void)[]> = new WeakMap();
|
||||||
@@ -38,13 +38,23 @@ function _handleError(node: ComponentNode | null, error: Error, isFirstRound = f
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleError(node: ComponentNode, error: Error) {
|
export function handleError(entity: ComponentNode | Fiber, error: Error) {
|
||||||
const fiber = node.fiber!;
|
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);
|
fibersInError.set(fiber.root, error);
|
||||||
|
|
||||||
if (!_handleError(node, error, true)) {
|
const handled = _handleError(node, error, true);
|
||||||
|
if (!handled) {
|
||||||
try {
|
try {
|
||||||
node.app.destroy();
|
node.app.destroy();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
return handled;
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-57
@@ -2,7 +2,7 @@ import type { BDom } from "../blockdom";
|
|||||||
import { mount } from "../blockdom";
|
import { mount } from "../blockdom";
|
||||||
import type { ComponentNode } from "./component_node";
|
import type { ComponentNode } from "./component_node";
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
import { fibersInError } from "./error_handling";
|
import { fibersInError, handleError } from "./error_handling";
|
||||||
|
|
||||||
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
||||||
let current = node.fiber;
|
let current = node.fiber;
|
||||||
@@ -106,56 +106,63 @@ export class RootFiber extends Fiber {
|
|||||||
|
|
||||||
complete() {
|
complete() {
|
||||||
const node = this.node;
|
const node = this.node;
|
||||||
|
let current: Fiber | undefined = undefined;
|
||||||
// Step 1: calling all willPatch lifecycle hooks
|
try {
|
||||||
for (let fiber of this.willPatch) {
|
// Step 1: calling all willPatch lifecycle hooks
|
||||||
// because of the asynchronous nature of the rendering, some parts of the
|
for (current of this.willPatch) {
|
||||||
// UI may have been rendered, then deleted in a followup rendering, and we
|
// because of the asynchronous nature of the rendering, some parts of the
|
||||||
// do not want to call onWillPatch in that case.
|
// UI may have been rendered, then deleted in a followup rendering, and we
|
||||||
let node = fiber.node;
|
// do not want to call onWillPatch in that case.
|
||||||
if (node.fiber === fiber) {
|
let node = current.node;
|
||||||
const component = node.component;
|
if (node.fiber === current) {
|
||||||
for (let cb of node.willPatch) {
|
const component = node.component;
|
||||||
cb.call(component);
|
for (let cb of node.willPatch) {
|
||||||
|
cb.call(component);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
current = undefined;
|
||||||
|
|
||||||
// Step 2: patching the dom
|
// Step 2: patching the dom
|
||||||
node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0);
|
node.bdom!.patch(this.bdom!, Object.keys(node.children).length > 0);
|
||||||
this.appliedToDom = true;
|
this.appliedToDom = true;
|
||||||
|
|
||||||
// Step 3: calling all destroyed hooks
|
// Step 3: calling all destroyed hooks
|
||||||
for (let node of __internal__destroyed) {
|
for (let node of __internal__destroyed) {
|
||||||
for (let cb of node.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) {
|
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
__internal__destroyed.length = 0;
|
||||||
|
|
||||||
// Step 5: calling all patched hooks
|
// Step 4: calling all mounted lifecycle hooks
|
||||||
let patchedFibers = this.patched;
|
let mountedFibers = this.mounted;
|
||||||
while ((current = patchedFibers.pop())) {
|
while ((current = mountedFibers.pop())) {
|
||||||
if (current.appliedToDom) {
|
current = current;
|
||||||
for (let cb of current.node.patched) {
|
if (current.appliedToDom) {
|
||||||
cb();
|
for (let cb of current.node.mounted) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// unregistering the fiber
|
// Step 5: calling all patched hooks
|
||||||
node.fiber = null;
|
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";
|
this.position = options.position || "last-child";
|
||||||
}
|
}
|
||||||
complete() {
|
complete() {
|
||||||
const node = this.node;
|
let current: Fiber | undefined = this;
|
||||||
node.bdom = this.bdom;
|
try {
|
||||||
if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
const node = this.node;
|
||||||
mount(node.bdom!, this.target);
|
node.bdom = this.bdom;
|
||||||
} else {
|
if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
||||||
const firstChild = this.target.childNodes[0];
|
mount(node.bdom!, this.target);
|
||||||
mount(node.bdom!, this.target, firstChild);
|
} else {
|
||||||
}
|
const firstChild = this.target.childNodes[0];
|
||||||
node.status = STATUS.MOUNTED;
|
mount(node.bdom!, this.target, firstChild);
|
||||||
this.appliedToDom = true;
|
}
|
||||||
let current;
|
node.status = STATUS.MOUNTED;
|
||||||
let mountedFibers = this.mounted;
|
this.appliedToDom = true;
|
||||||
while ((current = mountedFibers.pop())) {
|
let mountedFibers = this.mounted;
|
||||||
if (current.appliedToDom) {
|
while ((current = mountedFibers.pop())) {
|
||||||
for (let cb of current.node.mounted) {
|
if (current.appliedToDom) {
|
||||||
cb();
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Fiber, RootFiber } from "./fibers";
|
import { Fiber, RootFiber } from "./fibers";
|
||||||
import { handleError, fibersInError } from "./error_handling";
|
import { fibersInError } from "./error_handling";
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -60,13 +60,8 @@ export class Scheduler {
|
|||||||
|
|
||||||
if (fiber.counter === 0) {
|
if (fiber.counter === 0) {
|
||||||
if (!hasError) {
|
if (!hasError) {
|
||||||
try {
|
fiber.complete();
|
||||||
fiber.complete();
|
fiber.resolve();
|
||||||
fiber.resolve();
|
|
||||||
} catch (e) {
|
|
||||||
handleError(fiber.node, e);
|
|
||||||
fiber.reject(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.tasks.delete(fiber);
|
this.tasks.delete(fiber);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(\`<div>Some text</div>\`);
|
||||||
|
|
||||||
|
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(\`<div><block-child-0/><block-child-1/><block-child-1/></div>\`);
|
||||||
|
|
||||||
|
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(\`<div><block-child-0/></div>\`);
|
||||||
|
|
||||||
|
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(\`<div><block-text-0/></div>\`);
|
||||||
|
|
||||||
|
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(\`<div><block-child-0/><block-child-1/><block-child-1/></div>\`);
|
||||||
|
|
||||||
|
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(\`<div><span><block-text-0/></span><block-child-0/></div>\`);
|
||||||
|
|
||||||
|
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`] = `
|
exports[`can catch errors can catch an error in the willStart call 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -564,33 +564,30 @@ describe("can catch errors", () => {
|
|||||||
console.error = consoleError;
|
console.error = consoleError;
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("can catch an error in the mounted call", async () => {
|
test("can catch an error in the mounted call", async () => {
|
||||||
// we do not catch error in mounted anymore
|
|
||||||
console.error = jest.fn();
|
|
||||||
// env.qweb.addTemplates(`
|
|
||||||
// <templates>
|
|
||||||
// <div t-name="ErrorBoundary">
|
|
||||||
// <t t-if="state.error">Error handled</t>
|
|
||||||
// <t t-else=""><t t-slot="default" /></t>
|
|
||||||
// </div>
|
|
||||||
// <div t-name="ErrorComponent">Some text</div>
|
|
||||||
// <div t-name="App">
|
|
||||||
// <ErrorBoundary><ErrorComponent /></ErrorBoundary>
|
|
||||||
// </div>
|
|
||||||
// </templates>`);
|
|
||||||
class ErrorComponent extends Component {
|
class ErrorComponent extends Component {
|
||||||
mounted() {
|
static template = xml`<div>Some text</div>`;
|
||||||
throw new Error("NOOOOO");
|
setup() {
|
||||||
|
onMounted(() => {
|
||||||
|
throw new Error("NOOOOO");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ErrorBoundary extends Component {
|
class ErrorBoundary extends Component {
|
||||||
|
static template = xml`<div>
|
||||||
|
<t t-if="state.error">Error handled</t>
|
||||||
|
<t t-else=""><t t-slot="default" /></t>
|
||||||
|
</div>`;
|
||||||
state = useState({ error: false });
|
state = useState({ error: false });
|
||||||
|
|
||||||
catchError() {
|
setup() {
|
||||||
this.state.error = true;
|
onError(() => (this.state.error = true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
|
static template = xml`<div>
|
||||||
|
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
|
||||||
|
</div>`;
|
||||||
static components = { ErrorBoundary, ErrorComponent };
|
static components = { ErrorBoundary, ErrorComponent };
|
||||||
}
|
}
|
||||||
await mount(App, fixture);
|
await mount(App, fixture);
|
||||||
@@ -600,10 +597,7 @@ describe("can catch errors", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("can catch an error in the willPatch call", async () => {
|
test("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();
|
|
||||||
class ErrorComponent extends Component {
|
class ErrorComponent extends Component {
|
||||||
static template = xml`<div><t t-esc="props.message"/></div>`;
|
static template = xml`<div><t t-esc="props.message"/></div>`;
|
||||||
setup() {
|
setup() {
|
||||||
@@ -620,8 +614,8 @@ describe("can catch errors", () => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
state = useState({ error: false });
|
state = useState({ error: false });
|
||||||
|
|
||||||
catchError() {
|
setup() {
|
||||||
this.state.error = true;
|
onError(() => (this.state.error = true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
@@ -640,8 +634,6 @@ describe("can catch errors", () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div><span>def</span><div>Error handled</div></div>");
|
expect(fixture.innerHTML).toBe("<div><span>def</span><div>Error handled</div></div>");
|
||||||
expect(console.error).toHaveBeenCalledTimes(1);
|
|
||||||
console.error = consoleError;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("catchError in catchError", async () => {
|
test("catchError in catchError", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user