[IMP] *: use a custom error class for all errors thrown by owl

This commit makes all errors thrown in owl use a custom error class. The
main point of this is to always wrap user-code errors that happen during
the owl lifecycle so that they can be treated uniformly in onError by
checking the cause property, and also allows user code to differenciate
owl errors from non-owl errors reliably at runtime.
This commit is contained in:
Samuel Degueldre
2022-07-18 10:41:50 +02:00
committed by Géry Debongnie
parent 30bc605c84
commit 7786077921
24 changed files with 261 additions and 100 deletions
@@ -101,6 +101,50 @@ exports[`basics simple catchError 2`] = `
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: async hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: sync hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped out of dev mode: async hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped outside dev mode: sync hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors an error in onWillDestroy 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1257,19 +1301,6 @@ exports[`errors and promises an error in willPatch call will reject the render p
}"
`;
exports[`errors and promises error type is kept when it is wrapped 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>abc</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`errors and promises errors in mounted and in willUnmount 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -43,7 +43,7 @@ exports[`list of components crash on duplicate key in dev mode 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
let { prepareList, OwlError, withKey } = helpers;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
return function template(ctx, node, key = \\"\\") {
@@ -53,7 +53,7 @@ exports[`list of components crash on duplicate key in dev mode 1`] = `
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
const key1 = 'child';
if (keys1.has(key1)) { throw new Error(\`Got duplicate key in t-foreach: \${key1}\`)}
if (keys1.has(key1)) { throw new OwlError(\`Got duplicate key in t-foreach: \${key1}\`)}
keys1.add(key1);
const props1 = {};
helpers.validateProps(\`Child\`, props1, ctx);
+110 -15
View File
@@ -19,6 +19,7 @@ import {
snapshotEverything,
useLogLifecycle,
} from "../helpers";
import { OwlError } from "../../src/runtime/error_handling";
let fixture: HTMLElement;
@@ -159,16 +160,17 @@ describe("errors and promises", () => {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
let error: Error;
let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error!.message).toMatch(regexp);
expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleError).toBeCalledTimes(0);
});
@@ -183,14 +185,15 @@ describe("errors and promises", () => {
}
}
let error: Error;
let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.message).toBe("boom");
expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
@@ -344,16 +347,17 @@ describe("errors and promises", () => {
static components = { Child };
}
let error: Error;
let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error!.message).toMatch(regexp);
expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
});
@@ -363,7 +367,7 @@ describe("errors and promises", () => {
static template = xml`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
flag = false;
setup() {
onError((e) => (error = e));
onError(({ cause }) => (error = cause));
}
}
@@ -390,16 +394,17 @@ describe("errors and promises", () => {
static components = { Child };
}
let error: Error;
let error: OwlError;
try {
await mount(Parent, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
expect(error!.message).toMatch(regexp);
expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
});
@@ -506,6 +511,96 @@ describe("can catch errors", () => {
);
});
test("Errors in owl lifecycle are wrapped in dev mode: sync hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture, { test: true });
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe(`The following error occurred in onMounted: "test error"`);
expect(e!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped in dev mode: async hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onWillStart(async () => {
await nextMicroTick();
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture, { test: true });
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe(`The following error occurred in onWillStart: "test error"`);
expect(e!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped outside dev mode: sync hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture);
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped out of dev mode: async hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onWillStart(async () => {
await nextMicroTick();
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture);
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.cause).toBe(err);
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
@@ -1204,8 +1299,8 @@ describe("can catch errors", () => {
class Catch extends Component {
static template = xml`<t t-slot="default" />`;
setup() {
onError((error) => {
this.props.onError(error);
onError(({ cause }) => {
this.props.onError(cause);
});
}
}
+1 -1
View File
@@ -653,7 +653,7 @@ describe("hooks", () => {
try {
await mount(MyComponent, fixture);
} catch (e: any) {
expect(e.message).toBe("Intentional error");
expect(e.cause.message).toBe("Intentional error");
}
// no console.error because the error has been caught in this test
expect(console.error).toHaveBeenCalledTimes(0);
+5 -3
View File
@@ -1,3 +1,4 @@
import { OwlError } from "../../src/runtime/error_handling";
import { Component, mount, onMounted, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
@@ -346,16 +347,17 @@ describe("style and class handling", () => {
static template = xml`<Child class="'a'"/>`;
static components = { Child };
}
let error: Error;
let error: OwlError;
try {
await mount(ParentWidget, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error!.message).toMatch(regexp);
expect(error!.cause.message).toMatch(regexp);
expect(fixture.innerHTML).toBe("");
expect(mockConsoleWarn).toBeCalledTimes(1);
});
+2 -1
View File
@@ -19,6 +19,7 @@ import { helpers } from "../src/runtime/template_helpers";
import { TemplateSet, globalTemplates } from "../src/runtime/template_set";
import { BDom } from "../src/runtime/blockdom";
import { compile } from "../src/compiler";
import { OwlError } from "../src/runtime/error_handling";
const mount = blockDom.mount;
@@ -221,7 +222,7 @@ export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, v
afterEach(() => {
if (steps.length) {
steps.splice(0);
throw new Error("Remaining steps! Should be checked by a .toBeLogged() assertion!");
throw new OwlError("Remaining steps! Should be checked by a .toBeLogged() assertion!");
}
});
+6 -4
View File
@@ -1,3 +1,4 @@
import { OwlError } from "../../src/runtime/error_handling";
import {
App,
Component,
@@ -499,7 +500,7 @@ describe("Portal", () => {
</div>`;
state = { error: false };
setup() {
onError((e) => (error = e));
onError(({ cause }) => (error = cause));
}
}
addOutsideDiv(fixture);
@@ -958,14 +959,15 @@ describe("Portal: Props validation", () => {
</t>
</div>`;
}
let error: Error;
let error: OwlError;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.message).toBe(`' ' is not a valid selector`);
expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe(`' ' is not a valid selector`);
});
test("target must be a valid selector 2", async () => {