[IMP] app: rethrow errors that were not handled

This commit makes it so that when an error occurs in an owl app and none
of the registered error handlers are able to handle it, we rethrow the
error instead of just logging it to the console and swallowing it. This
allows users of owl to handle errors that happen in owl applications by
using event listeners for error and unhandledrejection events on the
window.
This commit is contained in:
Samuel Degueldre
2022-09-08 13:56:35 +02:00
committed by Géry Debongnie
parent a5a6a592c1
commit cfdf7caa50
14 changed files with 324 additions and 303 deletions
+6 -3
View File
@@ -6,6 +6,7 @@ import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers"; import { validateProps } from "./template_helpers";
import { TemplateSet, TemplateSetConfig } from "./template_set"; import { TemplateSet, TemplateSetConfig } from "./template_set";
import { validateTarget } from "./utils"; import { validateTarget } from "./utils";
import { handleError } from "./error_handling";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f // reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
@@ -94,9 +95,7 @@ export class App<
nodeErrorHandlers.set(node, handlers); nodeErrorHandlers.set(node, handlers);
} }
handlers.unshift((e) => { handlers.unshift((e) => {
if (isResolved) { if (!isResolved) {
console.error(e);
} else {
reject(e); reject(e);
} }
throw e; throw e;
@@ -169,6 +168,10 @@ export class App<
return node; return node;
}; };
} }
handleError(...args: Parameters<typeof handleError>) {
return handleError(...args);
}
} }
export async function mount< export async function mount<
+3 -3
View File
@@ -1,7 +1,7 @@
import type { App, Env } from "./app"; import type { App, Env } from "./app";
import { BDom, VNode } from "./blockdom"; import { BDom, VNode } from "./blockdom";
import { Component, ComponentConstructor, Props } from "./component"; import { Component, ComponentConstructor, Props } from "./component";
import { fibersInError, handleError, OwlError } from "./error_handling"; import { fibersInError, OwlError } from "./error_handling";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers"; import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import { import {
clearReactivesForCallback, clearReactivesForCallback,
@@ -141,7 +141,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
try { try {
await Promise.all(this.willStart.map((f) => f.call(component))); await Promise.all(this.willStart.map((f) => f.call(component)));
} catch (e) { } catch (e) {
handleError({ node: this, error: e }); this.app.handleError({ node: this, error: e });
return; return;
} }
if (this.status === STATUS.NEW && this.fiber === fiber) { if (this.status === STATUS.NEW && this.fiber === fiber) {
@@ -219,7 +219,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
cb.call(component); cb.call(component);
} }
} catch (e) { } catch (e) {
handleError({ error: e, node: this }); this.app.handleError({ error: e, node: this });
} }
} }
this.status = STATUS.DESTROYED; this.status = STATUS.DESTROYED;
+1
View File
@@ -71,5 +71,6 @@ export function handleError(params: ErrorParams) {
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
throw error;
} }
} }
+4 -4
View File
@@ -1,6 +1,6 @@
import { BDom, mount } from "./blockdom"; import { BDom, mount } from "./blockdom";
import type { ComponentNode } from "./component_node"; import type { ComponentNode } from "./component_node";
import { fibersInError, handleError, OwlError } from "./error_handling"; import { fibersInError, OwlError } from "./error_handling";
import { STATUS } from "./status"; import { STATUS } from "./status";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber { export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
@@ -130,7 +130,7 @@ export class Fiber {
(this.bdom as any) = true; (this.bdom as any) = true;
this.bdom = node.renderFn(); this.bdom = node.renderFn();
} catch (e) { } catch (e) {
handleError({ node, error: e }); node.app.handleError({ node, error: e });
} }
root.setCounter(root.counter - 1); root.setCounter(root.counter - 1);
} }
@@ -195,7 +195,7 @@ export class RootFiber extends Fiber {
} }
} catch (e) { } catch (e) {
this.locked = false; this.locked = false;
handleError({ fiber: current || this, error: e }); node.app.handleError({ fiber: current || this, error: e });
} }
} }
@@ -259,7 +259,7 @@ export class MountFiber extends RootFiber {
} }
} }
} catch (e) { } catch (e) {
handleError({ fiber: current as Fiber, error: e }); this.node.app.handleError({ fiber: current as Fiber, error: e });
} }
} }
} }
+14 -7
View File
@@ -1,5 +1,12 @@
import { App, Component, mount, status, toRaw, useState, xml } from "../../src"; import { App, Component, mount, status, toRaw, useState, xml } from "../../src";
import { elem, makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers"; import {
elem,
makeTestFixture,
nextAppError,
nextTick,
snapshotEverything,
useLogLifecycle,
} from "../helpers";
import { markup } from "../../src/runtime/utils"; import { markup } from "../../src/runtime/utils";
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -208,14 +215,14 @@ describe("basics", () => {
static template = xml`<div/>`; static template = xml`<div/>`;
} }
let error: Error; let error: Error;
const prom = mount(Test, fixture); const app = new App(Test);
const prom = app.mount(fixture);
await Promise.resolve(); await Promise.resolve();
fixture.remove(); fixture.remove();
try { prom.catch((e: Error) => (error = e));
await prom; await expect(nextAppError(app)).resolves.toThrow(
} catch (e) { "Cannot mount a component on a detached dom node"
error = e as Error; );
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount a component on a detached dom node"); expect(error!.message).toBe("Cannot mount a component on a detached dom node");
expect(console.warn).toBeCalledTimes(1); expect(console.warn).toBeCalledTimes(1);
+117 -127
View File
@@ -1,4 +1,4 @@
import { Component, mount, onWillDestroy } from "../../src"; import { App, Component, mount, onWillDestroy } from "../../src";
import { import {
onError, onError,
onMounted, onMounted,
@@ -18,6 +18,7 @@ import {
nextMicroTick, nextMicroTick,
snapshotEverything, snapshotEverything,
useLogLifecycle, useLogLifecycle,
nextAppError,
} from "../helpers"; } from "../helpers";
import { OwlError } from "../../src/runtime/error_handling"; import { OwlError } from "../../src/runtime/error_handling";
@@ -59,9 +60,10 @@ describe("basics", () => {
parent.state.flag = true; parent.state.flag = true;
parent.render(); parent.render();
await nextTick(); await expect(nextAppError(parent.__owl__.app)).resolves.toThrow(
"An error occured in the owl lifecycle"
);
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(1);
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
}); });
@@ -71,12 +73,13 @@ describe("basics", () => {
static template = xml`<SomeMispelledComponent />`; static template = xml`<SomeMispelledComponent />`;
static components = { SomeComponent }; static components = { SomeComponent };
} }
const app = new App(Parent);
let error: Error; let error: Error;
try { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await mount(Parent, fixture); await expect(nextAppError(app)).resolves.toThrow(
} catch (e) { 'Cannot find the definition of component "SomeMispelledComponent"'
error = e as Error; );
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"'); expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"');
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
@@ -90,12 +93,13 @@ describe("basics", () => {
static template = xml`<SomeMispelledComponent />`; static template = xml`<SomeMispelledComponent />`;
static components = { SomeComponent }; static components = { SomeComponent };
} }
const app = new App(Parent, { test: true });
let error: Error; let error: Error;
try { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await mount(Parent, fixture, { test: true }); await expect(nextAppError(app)).resolves.toThrow(
} catch (e) { 'Cannot find the definition of component "SomeMispelledComponent"'
error = e as Error; );
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"'); expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"');
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
@@ -109,13 +113,13 @@ describe("basics", () => {
static template = xml`<SomeComponent />`; static template = xml`<SomeComponent />`;
static components = { SomeComponent: notAComponentConstructor }; static components = { SomeComponent: notAComponentConstructor };
} }
const app = new App(Parent as typeof Component);
let error: Error; let error: Error;
try { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
// @ts-expect-error await expect(nextAppError(app)).resolves.toThrow(
await mount(Parent, fixture); '"SomeComponent" is not a Component. It must inherit from the Component class'
} catch (e) { );
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
'"SomeComponent" is not a Component. It must inherit from the Component class' '"SomeComponent" is not a Component. It must inherit from the Component class'
@@ -156,16 +160,15 @@ describe("basics", () => {
describe("errors and promises", () => { describe("errors and promises", () => {
test("a rendering error will reject the mount promise", async () => { test("a rendering error will reject the mount promise", async () => {
// we do not catch error in willPatch anymore // we do not catch error in willPatch anymore
class App extends Component { class Root extends Component {
static template = xml`<div><t t-esc="this.will.crash"/></div>`; static template = xml`<div><t t-esc="this.will.crash"/></div>`;
} }
const app = new App(Root);
let error: OwlError; let error: OwlError;
try { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await mount(App, fixture); await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
} catch (e) { await mountProm;
error = e as OwlError;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined(); expect(error!.cause).toBeDefined();
const regexp = const regexp =
@@ -176,7 +179,7 @@ describe("errors and promises", () => {
}); });
test("an error in mounted call will reject the mount promise", async () => { test("an error in mounted call will reject the mount promise", async () => {
class App extends Component { class Root extends Component {
static template = xml`<div>abc</div>`; static template = xml`<div>abc</div>`;
setup() { setup() {
onMounted(() => { onMounted(() => {
@@ -185,12 +188,11 @@ describe("errors and promises", () => {
} }
} }
const app = new App(Root);
let error: OwlError; let error: OwlError;
try { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await mount(App, fixture); await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
} catch (e) { await mountProm;
error = e as OwlError;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined(); expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe("boom"); expect(error!.cause.message).toBe("boom");
@@ -200,7 +202,7 @@ describe("errors and promises", () => {
}); });
test("an error in onMounted callback will have the component's setup in its stack trace", async () => { test("an error in onMounted callback will have the component's setup in its stack trace", async () => {
class App extends Component { class Root extends Component {
static template = xml`<div>abc</div>`; static template = xml`<div>abc</div>`;
setup() { setup() {
onMounted(() => { onMounted(() => {
@@ -209,14 +211,13 @@ describe("errors and promises", () => {
} }
} }
let error: Error; const app = new App(Root, { test: true });
try { let error: OwlError;
await mount(App, fixture, { test: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted");
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.stack).toContain("App.setup"); expect(error!.stack).toContain("Root.setup");
expect(error!.stack).toContain("error_handling.test.ts"); expect(error!.stack).toContain("error_handling.test.ts");
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0);
@@ -224,7 +225,7 @@ describe("errors and promises", () => {
}); });
test("errors in onWillRender/onRender aren't wrapped more than once", async () => { test("errors in onWillRender/onRender aren't wrapped more than once", async () => {
class App extends Component { class Root extends Component {
static template = xml`<div>abc</div>`; static template = xml`<div>abc</div>`;
setup() { setup() {
onWillRender(() => { onWillRender(() => {
@@ -236,12 +237,11 @@ describe("errors and promises", () => {
} }
} }
let error: Error; const app = new App(Root, { test: true });
try { let error: OwlError;
await mount(App, fixture, { test: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillRender");
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
`The following error occurred in onWillRender: "boom in onWillRender"` `The following error occurred in onWillRender: "boom in onWillRender"`
@@ -278,12 +278,11 @@ describe("errors and promises", () => {
} }
} }
let error: any; const app = new App(Root, { test: true });
try { let error: OwlError;
await mount(Root, fixture, { test: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart");
error = e; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
`The following error occurred in onWillStart: "boom in onWillStart"` `The following error occurred in onWillStart: "boom in onWillStart"`
@@ -342,17 +341,16 @@ describe("errors and promises", () => {
class Child extends Component { class Child extends Component {
static template = xml`<div><t t-esc="this.will.crash"/></div>`; static template = xml`<div><t t-esc="this.will.crash"/></div>`;
} }
class App extends Component { class Parent extends Component {
static template = xml`<div><Child/></div>`; static template = xml`<div><Child/></div>`;
static components = { Child }; static components = { Child };
} }
const app = new App(Parent);
let error: OwlError; let error: OwlError;
try { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await mount(App, fixture); await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
} catch (e) { await mountProm;
error = e as OwlError;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined(); expect(error!.cause).toBeDefined();
const regexp = const regexp =
@@ -394,12 +392,11 @@ describe("errors and promises", () => {
static components = { Child }; static components = { Child };
} }
const app = new App(Parent);
let error: OwlError; let error: OwlError;
try { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await mount(Parent, fixture); await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
} catch (e) { await mountProm;
error = e as OwlError;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined(); expect(error!.cause).toBeDefined();
const regexp = const regexp =
@@ -425,13 +422,12 @@ describe("errors and promises", () => {
} }
} }
try { const app = new App(Example, { test: true });
await mount(Example, fixture, { test: true }); let error: OwlError;
} catch (e) { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
expect((e as Error).message).toBe( await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted");
`The following error occurred in onMounted: "Error in mounted"` await mountProm;
); expect(error!.message).toBe(`The following error occurred in onMounted: "Error in mounted"`);
}
// 1 additional error is logged because the destruction of the app causes // 1 additional error is logged because the destruction of the app causes
// the onWillUnmount hook to be called and to fail // the onWillUnmount hook to be called and to fail
expect(mockConsoleError).toBeCalledTimes(1); expect(mockConsoleError).toBeCalledTimes(1);
@@ -448,9 +444,10 @@ describe("errors and promises", () => {
root.state = "boom"; root.state = "boom";
root.render(); root.render();
await nextTick(); await expect(nextAppError(root.__owl__.app)).resolves.toThrow(
"error occured in the owl lifecycle"
);
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(1);
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
}); });
}); });
@@ -500,13 +497,12 @@ describe("can catch errors", () => {
}); });
} }
} }
let e: Error; const app = new App(Root, { test: true });
try { let error: OwlError;
await mount(Root, fixture, { test: true }); const crashProm = expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart");
} catch (error) { await app.mount(fixture).catch((e: Error) => (error = e));
e = error as Error; await crashProm;
} expect(error!.message).toBe(
expect(e!.message).toBe(
`The following error occurred in onWillStart: "No active component (a hook function should only be called in 'setup')"` `The following error occurred in onWillStart: "No active component (a hook function should only be called in 'setup')"`
); );
}); });
@@ -523,14 +519,13 @@ describe("can catch errors", () => {
}); });
} }
} }
let e: OwlError; const app = new App(Root, { test: true });
try { let error: OwlError;
await mount(Root, fixture, { test: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (error) { await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted");
e = error as OwlError; await mountProm;
} expect(error!.message).toBe(`The following error occurred in onMounted: "test error"`);
expect(e!.message).toBe(`The following error occurred in onMounted: "test error"`); expect(error!.cause).toBe(err);
expect(e!.cause).toBe(err);
}); });
test("Errors in owl lifecycle are wrapped in dev mode: async hook", async () => { test("Errors in owl lifecycle are wrapped in dev mode: async hook", async () => {
@@ -546,14 +541,13 @@ describe("can catch errors", () => {
}); });
} }
} }
let e: OwlError; const app = new App(Root, { test: true });
try { let error: OwlError;
await mount(Root, fixture, { test: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (error) { await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart");
e = error as OwlError; await mountProm;
} expect(error!.message).toBe(`The following error occurred in onWillStart: "test error"`);
expect(e!.message).toBe(`The following error occurred in onWillStart: "test error"`); expect(error!.cause).toBe(err);
expect(e!.cause).toBe(err);
}); });
test("Errors in owl lifecycle are wrapped outside dev mode: sync hook", async () => { test("Errors in owl lifecycle are wrapped outside dev mode: sync hook", async () => {
@@ -568,16 +562,15 @@ describe("can catch errors", () => {
}); });
} }
} }
let e: OwlError; const app = new App(Root);
try { let error: OwlError;
await mount(Root, fixture); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (error) { await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
e = error as OwlError; await mountProm;
} expect(error!.message).toBe(
expect(e!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)` `An error occured in the owl lifecycle (see this Error's "cause" property)`
); );
expect(e!.cause).toBe(err); expect(error!.cause).toBe(err);
}); });
test("Errors in owl lifecycle are wrapped out of dev mode: async hook", async () => { test("Errors in owl lifecycle are wrapped out of dev mode: async hook", async () => {
@@ -593,16 +586,15 @@ describe("can catch errors", () => {
}); });
} }
} }
let e: OwlError; const app = new App(Root);
try { let error: OwlError;
await mount(Root, fixture); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (error) { await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
e = error as OwlError; await mountProm;
} expect(error!.message).toBe(
expect(e!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)` `An error occured in the owl lifecycle (see this Error's "cause" property)`
); );
expect(e!.cause).toBe(err); expect(error!.cause).toBe(err);
}); });
test("Thrown values that are not errors are wrapped in dev mode", async () => { test("Thrown values that are not errors are wrapped in dev mode", async () => {
@@ -616,16 +608,15 @@ describe("can catch errors", () => {
}); });
} }
} }
let e: OwlError; const app = new App(Root, { test: true });
try { let error: OwlError;
await mount(Root, fixture, { test: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (error) { await expect(nextAppError(app)).resolves.toThrow("not an Error was thrown in onMounted");
e = error as OwlError; await mountProm;
} expect(error!.message).toBe(
expect(e!.message).toBe(
`Something that is not an Error was thrown in onMounted (see this Error's "cause" property)` `Something that is not an Error was thrown in onMounted (see this Error's "cause" property)`
); );
expect(e!.cause).toBe("This is not an error"); expect(error!.cause).toBe("This is not an error");
}); });
test("Thrown values that are not errors are wrapped outside dev mode", async () => { test("Thrown values that are not errors are wrapped outside dev mode", async () => {
@@ -639,16 +630,15 @@ describe("can catch errors", () => {
}); });
} }
} }
let e: OwlError; const app = new App(Root);
try { let error: OwlError;
await mount(Root, fixture); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (error) { await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
e = error as OwlError; await mountProm;
} expect(error!.message).toBe(
expect(e!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)` `An error occured in the owl lifecycle (see this Error's "cause" property)`
); );
expect(e!.cause).toBe("This is not an error"); expect(error!.cause).toBe("This is not an error");
}); });
test("can catch an error in the initial call of a component render function (parent mounted)", async () => { test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
+15 -6
View File
@@ -17,8 +17,16 @@ import {
useChildSubEnv, useChildSubEnv,
useSubEnv, useSubEnv,
xml, xml,
OwlError,
} from "../../src/index"; } from "../../src/index";
import { elem, logStep, makeTestFixture, nextTick, snapshotEverything } from "../helpers"; import {
elem,
logStep,
makeTestFixture,
nextAppError,
nextTick,
snapshotEverything,
} from "../helpers";
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -650,11 +658,12 @@ describe("hooks", () => {
} }
} }
try { let error: OwlError;
await mount(MyComponent, fixture); const app = new App(MyComponent);
} catch (e: any) { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
expect(e.cause.message).toBe("Intentional error"); await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
} await mountProm;
expect(error!.cause.message).toBe("Intentional error");
// no console.error because the error has been caught in this test // no console.error because the error has been caught in this test
expect(console.error).toHaveBeenCalledTimes(0); expect(console.error).toHaveBeenCalledTimes(0);
console.error = originalconsoleError; console.error = originalconsoleError;
+90 -103
View File
@@ -1,6 +1,6 @@
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
import { Component, onError, xml, mount } from "../../src"; import { Component, onError, xml, mount, OwlError } from "../../src";
import { DEV_MSG } from "../../src/runtime/app"; import { App, DEV_MSG } from "../../src/runtime/app";
import { validateProps } from "../../src/runtime/template_helpers"; import { validateProps } from "../../src/runtime/template_helpers";
import { Schema } from "../../src/runtime/validation"; import { Schema } from "../../src/runtime/validation";
@@ -48,13 +48,14 @@ describe("props validation", () => {
static components = { SubComp }; static components = { SubComp };
static template = xml`<div><SubComp /></div>`; static template = xml`<div><SubComp /></div>`;
} }
let error: Error | undefined;
try { const app = new App(Parent, { test: true });
await mount(Parent, fixture, { dev: true }); let error: OwlError | undefined;
} catch (e) { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow(
} "Invalid props for component 'SubComp': 'message' is missing"
);
await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing"); expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing");
error = undefined; error = undefined;
@@ -77,12 +78,13 @@ describe("props validation", () => {
static template = xml`<div><SubComp /></div>`; static template = xml`<div><SubComp /></div>`;
} }
let error: Error; const app = new App(Parent, { test: true });
try { let error: OwlError | undefined;
await mount(Parent, fixture, { dev: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow(
error = e as Error; "Invalid props for component 'SubComp': 'message' is missing"
} );
await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing"); expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing");
}); });
@@ -126,14 +128,12 @@ describe("props validation", () => {
}; };
(Parent as any).components = { SubComp }; (Parent as any).components = { SubComp };
let error: Error | undefined;
props = {}; props = {};
let app = new App(Parent, { test: true });
try { let error: OwlError | undefined;
await mount(Parent, fixture, { dev: true }); let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
`Invalid props for component '_a': 'p' is undefined (should be a ${test.type.name.toLowerCase()})` `Invalid props for component '_a': 'p' is undefined (should be a ${test.type.name.toLowerCase()})`
@@ -147,11 +147,10 @@ describe("props validation", () => {
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
props = { p: test.ko }; props = { p: test.ko };
try { app = new App(Parent, { test: true });
await mount(Parent, fixture, { dev: true }); mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
`Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}` `Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}`
@@ -181,13 +180,12 @@ describe("props validation", () => {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
}; };
(Parent as any).components = { SubComp }; (Parent as any).components = { SubComp };
let error: Error | undefined;
props = {}; props = {};
try { let app = new App(Parent, { test: true });
await mount(Parent, fixture, { dev: true }); let error: OwlError | undefined;
} catch (e) { let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
`Invalid props for component '_a': 'p' is undefined (should be a ${test.type.name.toLowerCase()})` `Invalid props for component '_a': 'p' is undefined (should be a ${test.type.name.toLowerCase()})`
@@ -201,11 +199,10 @@ describe("props validation", () => {
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
props = { p: test.ko }; props = { p: test.ko };
try { app = new App(Parent, { test: true });
await mount(Parent, fixture, { dev: true }); mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
`Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}` `Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}`
@@ -227,26 +224,25 @@ describe("props validation", () => {
} }
let error: Error; let error: Error;
let props: { p?: any }; let props: { p?: any };
try {
props = { p: "string" }; props = { p: "string" };
try {
await mount(Parent, fixture, { dev: true }); await mount(Parent, fixture, { dev: true });
} catch (e) { } catch (e) {
error = e as Error; error = e as Error;
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
try {
props = { p: true }; props = { p: true };
try {
await mount(Parent, fixture, { dev: true }); await mount(Parent, fixture, { dev: true });
} catch (e) { } catch (e) {
error = e as Error; error = e as Error;
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
try {
props = { p: 1 }; props = { p: 1 };
await mount(Parent, fixture, { dev: true }); const app = new App(Parent, { test: true });
} catch (e) { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' is not a string or boolean" "Invalid props for component 'SubComp': 'p' is not a string or boolean"
@@ -267,26 +263,25 @@ describe("props validation", () => {
} }
let error: Error; let error: Error;
let props: { p?: any }; let props: { p?: any };
try {
props = { p: "key" }; props = { p: "key" };
try {
await mount(Parent, fixture, { dev: true }); await mount(Parent, fixture, { dev: true });
} catch (e) { } catch (e) {
error = e as Error; error = e as Error;
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
try {
props = {}; props = {};
try {
await mount(Parent, fixture, { dev: true }); await mount(Parent, fixture, { dev: true });
} catch (e) { } catch (e) {
error = e as Error; error = e as Error;
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
try {
props = { p: 1 }; props = { p: 1 };
await mount(Parent, fixture, { dev: true }); const app = new App(Parent, { test: true });
} catch (e) { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is not a string"); expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is not a string");
}); });
@@ -319,20 +314,18 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
try {
props = { p: [1] }; props = { p: [1] };
await mount(Parent, fixture, { dev: true }); let app = new App(Parent, { test: true });
} catch (e) { let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
error = undefined; error = undefined;
try { app = new App(Parent, { test: true });
props = { p: ["string", 1] }; mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await mount(Parent, fixture, { dev: true }); await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
} catch (e) { await mountProm;
error = e as Error; expect(error!).toBeDefined();
}
}); });
test("can validate an array with multiple sub element types", async () => { test("can validate an array with multiple sub element types", async () => {
@@ -370,12 +363,11 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
try {
props = { p: [true, 1] }; props = { p: [true, 1] };
await mount(Parent, fixture, { dev: true }); const app = new App(Parent, { test: true });
} catch (e) { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p[1]' is not a string or boolean" "Invalid props for component 'SubComp': 'p[1]' is not a string or boolean"
@@ -405,33 +397,30 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
try {
props = { p: { id: 1, url: "url", extra: true } }; props = { p: { id: 1, url: "url", extra: true } };
await mount(Parent, fixture, { dev: true }); let app = new App(Parent, { test: true });
} catch (e) { let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' has not the correct shape (unknown key 'extra')" "Invalid props for component 'SubComp': 'p' has not the correct shape (unknown key 'extra')"
); );
try {
props = { p: { id: "1", url: "url" } }; props = { p: { id: "1", url: "url" } };
await mount(Parent, fixture, { dev: true }); app = new App(Parent, { test: true });
} catch (e) { mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' has not the correct shape ('id' is not a number)" "Invalid props for component 'SubComp': 'p' has not the correct shape ('id' is not a number)"
); );
error = undefined; error = undefined;
try {
props = { p: { id: 1 } }; props = { p: { id: 1 } };
await mount(Parent, fixture, { dev: true }); app = new App(Parent, { test: true });
} catch (e) { mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is missing (should be a string))" "Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is missing (should be a string))"
@@ -474,12 +463,11 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeUndefined(); expect(error!).toBeUndefined();
try {
props = { p: { id: 1, url: [12, true] } }; props = { p: { id: 1, url: [12, true] } };
await mount(Parent, fixture, { dev: true }); const app = new App(Parent, { test: true });
} catch (e) { const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
error = e as Error; await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
} await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is not a boolean or list of numbers)" "Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is not a boolean or list of numbers)"
@@ -686,11 +674,10 @@ describe("props validation", () => {
static components = { SubComp }; static components = { SubComp };
} }
let error: Error; let error: Error;
try { const app = new App(Parent, { test: true });
await mount(Parent, fixture, { dev: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is missing"); expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is missing");
}); });
@@ -754,11 +741,10 @@ describe("props validation", () => {
static template = xml`<div><Child/></div>`; static template = xml`<div><Child/></div>`;
} }
let error: Error; let error: Error;
try { const app = new App(Parent, { test: true });
await mount(Parent, fixture, { dev: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'Child'");
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
"Invalid props for component 'Child': 'mandatory' is missing (should be a number)" "Invalid props for component 'Child': 'mandatory' is missing (should be a number)"
@@ -859,11 +845,12 @@ describe("default props", () => {
static template = xml`<Child/>`; static template = xml`<Child/>`;
} }
let error: Error; let error: Error;
try { const app = new App(Parent, { test: true });
await mount(Parent, fixture, { dev: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow(
error = e as Error; "default value cannot be defined for a mandatory prop"
} );
await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( expect(error!.message).toBe(
"A default value cannot be defined for a mandatory prop (name: 'mandatory', component: Child)" "A default value cannot be defined for a mandatory prop (name: 'mandatory', component: Child)"
+10 -5
View File
@@ -1,5 +1,5 @@
import { Component, mount, onMounted, useRef, useState } from "../../src/index"; import { App, Component, mount, onMounted, useRef, useState } from "../../src/index";
import { logStep, makeTestFixture, nextTick, snapshotEverything } from "../helpers"; import { logStep, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
import { xml } from "../../src/index"; import { xml } from "../../src/index";
snapshotEverything(); snapshotEverything();
@@ -94,9 +94,14 @@ describe("refs", () => {
ref = useRef("coucou"); ref = useRef("coucou");
} }
await expect(async () => { const app = new App(Test, { test: true });
await mount(Test, fixture); const mountProm = expect(app.mount(fixture)).rejects.toThrowError(
}).rejects.toThrowError("Cannot have 2 elements with same ref name at the same time"); "Cannot have 2 elements with same ref name at the same time"
);
await expect(nextAppError(app)).resolves.toThrow(
"Cannot have 2 elements with same ref name at the same time"
);
await mountProm;
expect(console.warn).toBeCalledTimes(1); expect(console.warn).toBeCalledTimes(1);
console.warn = consoleWarn; console.warn = consoleWarn;
}); });
+7 -8
View File
@@ -1,5 +1,5 @@
import { App, Component, mount, onMounted, useState, xml } from "../../src/index"; import { App, Component, mount, onMounted, useState, xml } from "../../src/index";
import { children, makeTestFixture, nextTick, snapshotEverything } from "../helpers"; import { children, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
snapshotEverything(); snapshotEverything();
let originalconsoleWarn = console.warn; let originalconsoleWarn = console.warn;
@@ -204,13 +204,12 @@ describe("slots", () => {
static components = { Child }; static components = { Child };
} }
let error = null; let error: Error;
try { const app = new App(Parent);
await mount(Parent, fixture); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
error = e; await mountProm;
} expect(error!).not.toBeNull();
expect(error).not.toBeNull();
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
}); });
+7 -8
View File
@@ -1,6 +1,6 @@
import { OwlError } from "../../src/runtime/error_handling"; import { OwlError } from "../../src/runtime/error_handling";
import { Component, mount, onMounted, useState, xml } from "../../src"; import { App, Component, mount, onMounted, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
snapshotEverything(); snapshotEverything();
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -343,16 +343,15 @@ describe("style and class handling", () => {
class Child extends Component { class Child extends Component {
static template = xml`<div t-att-class="props.class" t-esc="this.will.crash"/>`; static template = xml`<div t-att-class="props.class" t-esc="this.will.crash"/>`;
} }
class ParentWidget extends Component { class Parent extends Component {
static template = xml`<Child class="'a'"/>`; static template = xml`<Child class="'a'"/>`;
static components = { Child }; static components = { Child };
} }
let error: OwlError; let error: OwlError;
try { const app = new App(Parent);
await mount(ParentWidget, fixture); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
error = e as OwlError; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined(); expect(error!.cause).toBeDefined();
const regexp = const regexp =
+15 -5
View File
@@ -1,5 +1,11 @@
import { Component, mount, onMounted, useState, xml } from "../../src/index"; import { App, Component, mount, onMounted, useState, xml } from "../../src/index";
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers"; import {
makeTestFixture,
nextAppError,
nextTick,
snapshotEverything,
useLogLifecycle,
} from "../helpers";
snapshotEverything(); snapshotEverything();
@@ -315,9 +321,13 @@ describe("list of components", () => {
`; `;
static components = { Child }; static components = { Child };
} }
await expect(async () => {
await mount(Parent, fixture, { dev: true }); const app = new App(Parent, { test: true });
}).rejects.toThrowError("Got duplicate key in t-foreach: child"); const mountProm = expect(app.mount(fixture)).rejects.toThrow(
"Got duplicate key in t-foreach: child"
);
await expect(nextAppError(app)).resolves.toThrow("Got duplicate key in t-foreach: child");
await mountProm;
console.info = consoleInfo; console.info = consoleInfo;
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
}); });
+14
View File
@@ -261,6 +261,20 @@ expect.extend({
}, },
}); });
export function nextAppError(app: any) {
const { handleError } = app;
return new Promise((resolve) => {
app.handleError = (...args: Parameters<typeof handleError>) => {
try {
handleError.call(app, ...args);
} catch (e: any) {
app.handleError = handleError;
resolve(e);
}
};
});
}
declare global { declare global {
namespace jest { namespace jest {
interface Matchers<R> { interface Matchers<R> {
+13 -16
View File
@@ -12,7 +12,7 @@ import {
} from "../../src"; } from "../../src";
import { xml } from "../../src/"; import { xml } from "../../src/";
import { DEV_MSG } from "../../src/runtime/app"; import { DEV_MSG } from "../../src/runtime/app";
import { elem, makeTestFixture, nextTick, snapshotEverything } from "../helpers"; import { elem, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
let fixture: HTMLElement; let fixture: HTMLElement;
let originalconsoleWarn = console.warn; let originalconsoleWarn = console.warn;
@@ -269,11 +269,10 @@ describe("Portal", () => {
} }
let error: Error; let error: Error;
try { const app = new App(Parent);
await mount(Parent, fixture); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("invalid portal target");
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe("invalid portal target"); expect(error!.message).toBe("invalid portal target");
@@ -960,11 +959,10 @@ describe("Portal: Props validation", () => {
</div>`; </div>`;
} }
let error: OwlError; let error: OwlError;
try { const app = new App(Parent);
await mount(Parent, fixture, { dev: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
error = e as OwlError; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined(); expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe(`' ' is not a valid selector`); expect(error!.cause.message).toBe(`' ' is not a valid selector`);
@@ -980,11 +978,10 @@ describe("Portal: Props validation", () => {
</div>`; </div>`;
} }
let error: Error; let error: Error;
try { const app = new App(Parent);
await mount(Parent, fixture, { dev: true }); const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
} catch (e) { await expect(nextAppError(app)).resolves.toThrow("invalid portal target");
error = e as Error; await mountProm;
}
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe(`invalid portal target`); expect(error!.message).toBe(`invalid portal target`);
}); });