diff --git a/src/runtime/app.ts b/src/runtime/app.ts index f3adde96..736f221d 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -6,6 +6,7 @@ import { Scheduler } from "./scheduler"; import { validateProps } from "./template_helpers"; import { TemplateSet, TemplateSetConfig } from "./template_set"; import { validateTarget } from "./utils"; +import { handleError } from "./error_handling"; // reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f @@ -94,9 +95,7 @@ export class App< nodeErrorHandlers.set(node, handlers); } handlers.unshift((e) => { - if (isResolved) { - console.error(e); - } else { + if (!isResolved) { reject(e); } throw e; @@ -169,6 +168,10 @@ export class App< return node; }; } + + handleError(...args: Parameters) { + return handleError(...args); + } } export async function mount< diff --git a/src/runtime/component_node.ts b/src/runtime/component_node.ts index ef07901e..e6636001 100644 --- a/src/runtime/component_node.ts +++ b/src/runtime/component_node.ts @@ -1,7 +1,7 @@ import type { App, Env } from "./app"; import { BDom, VNode } from "./blockdom"; 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 { clearReactivesForCallback, @@ -141,7 +141,7 @@ export class ComponentNode

implements VNode f.call(component))); } catch (e) { - handleError({ node: this, error: e }); + this.app.handleError({ node: this, error: e }); return; } if (this.status === STATUS.NEW && this.fiber === fiber) { @@ -219,7 +219,7 @@ export class ComponentNode

implements VNode { static template = xml`

`; } let error: Error; - const prom = mount(Test, fixture); + const app = new App(Test); + const prom = app.mount(fixture); await Promise.resolve(); fixture.remove(); - try { - await prom; - } catch (e) { - error = e as Error; - } + prom.catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow( + "Cannot mount a component on a detached dom node" + ); expect(error!).toBeDefined(); expect(error!.message).toBe("Cannot mount a component on a detached dom node"); expect(console.warn).toBeCalledTimes(1); diff --git a/tests/components/error_handling.test.ts b/tests/components/error_handling.test.ts index 023cdb50..8a7e0d07 100644 --- a/tests/components/error_handling.test.ts +++ b/tests/components/error_handling.test.ts @@ -1,4 +1,4 @@ -import { Component, mount, onWillDestroy } from "../../src"; +import { App, Component, mount, onWillDestroy } from "../../src"; import { onError, onMounted, @@ -18,6 +18,7 @@ import { nextMicroTick, snapshotEverything, useLogLifecycle, + nextAppError, } from "../helpers"; import { OwlError } from "../../src/runtime/error_handling"; @@ -59,9 +60,10 @@ describe("basics", () => { parent.state.flag = true; parent.render(); - await nextTick(); + await expect(nextAppError(parent.__owl__.app)).resolves.toThrow( + "An error occured in the owl lifecycle" + ); expect(fixture.innerHTML).toBe(""); - expect(mockConsoleError).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1); }); @@ -71,12 +73,13 @@ describe("basics", () => { static template = xml``; static components = { SomeComponent }; } + const app = new App(Parent); let error: Error; - try { - await mount(Parent, fixture); - } catch (e) { - error = e as Error; - } + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow( + 'Cannot find the definition of component "SomeMispelledComponent"' + ); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"'); expect(console.error).toBeCalledTimes(0); @@ -90,12 +93,13 @@ describe("basics", () => { static template = xml``; static components = { SomeComponent }; } + const app = new App(Parent, { test: true }); let error: Error; - try { - await mount(Parent, fixture, { test: true }); - } catch (e) { - error = e as Error; - } + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow( + 'Cannot find the definition of component "SomeMispelledComponent"' + ); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"'); expect(console.error).toBeCalledTimes(0); @@ -109,13 +113,13 @@ describe("basics", () => { static template = xml``; static components = { SomeComponent: notAComponentConstructor }; } + const app = new App(Parent as typeof Component); let error: Error; - try { - // @ts-expect-error - await mount(Parent, fixture); - } catch (e) { - error = e as Error; - } + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow( + '"SomeComponent" is not a Component. It must inherit from the Component class' + ); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( '"SomeComponent" is not a Component. It must inherit from the Component class' @@ -156,16 +160,15 @@ describe("basics", () => { describe("errors and promises", () => { test("a rendering error will reject the mount promise", async () => { // we do not catch error in willPatch anymore - class App extends Component { + class Root extends Component { static template = xml`
`; } + const app = new App(Root); let error: OwlError; - try { - await mount(App, fixture); - } catch (e) { - error = e as OwlError; - } + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; expect(error!).toBeDefined(); expect(error!.cause).toBeDefined(); const regexp = @@ -176,7 +179,7 @@ describe("errors and promises", () => { }); test("an error in mounted call will reject the mount promise", async () => { - class App extends Component { + class Root extends Component { static template = xml`
abc
`; setup() { onMounted(() => { @@ -185,12 +188,11 @@ describe("errors and promises", () => { } } + const app = new App(Root); let error: OwlError; - try { - await mount(App, fixture); - } catch (e) { - error = e as OwlError; - } + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; expect(error!).toBeDefined(); expect(error!.cause).toBeDefined(); 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 () => { - class App extends Component { + class Root extends Component { static template = xml`
abc
`; setup() { onMounted(() => { @@ -209,14 +211,13 @@ describe("errors and promises", () => { } } - let error: Error; - try { - await mount(App, fixture, { test: true }); - } catch (e) { - error = e as Error; - } + const app = new App(Root, { test: true }); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted"); + await mountProm; expect(error!).toBeDefined(); - expect(error!.stack).toContain("App.setup"); + expect(error!.stack).toContain("Root.setup"); expect(error!.stack).toContain("error_handling.test.ts"); expect(fixture.innerHTML).toBe(""); expect(mockConsoleError).toBeCalledTimes(0); @@ -224,7 +225,7 @@ describe("errors and promises", () => { }); test("errors in onWillRender/onRender aren't wrapped more than once", async () => { - class App extends Component { + class Root extends Component { static template = xml`
abc
`; setup() { onWillRender(() => { @@ -236,12 +237,11 @@ describe("errors and promises", () => { } } - let error: Error; - try { - await mount(App, fixture, { test: true }); - } catch (e) { - error = e as Error; - } + const app = new App(Root, { test: true }); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillRender"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( `The following error occurred in onWillRender: "boom in onWillRender"` @@ -278,12 +278,11 @@ describe("errors and promises", () => { } } - let error: any; - try { - await mount(Root, fixture, { test: true }); - } catch (e) { - error = e; - } + const app = new App(Root, { test: true }); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( `The following error occurred in onWillStart: "boom in onWillStart"` @@ -342,17 +341,16 @@ describe("errors and promises", () => { class Child extends Component { static template = xml`
`; } - class App extends Component { + class Parent extends Component { static template = xml`
`; static components = { Child }; } + const app = new App(Parent); let error: OwlError; - try { - await mount(App, fixture); - } catch (e) { - error = e as OwlError; - } + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; expect(error!).toBeDefined(); expect(error!.cause).toBeDefined(); const regexp = @@ -394,12 +392,11 @@ describe("errors and promises", () => { static components = { Child }; } + const app = new App(Parent); let error: OwlError; - try { - await mount(Parent, fixture); - } catch (e) { - error = e as OwlError; - } + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; expect(error!).toBeDefined(); expect(error!.cause).toBeDefined(); const regexp = @@ -425,13 +422,12 @@ describe("errors and promises", () => { } } - try { - await mount(Example, fixture, { test: true }); - } catch (e) { - expect((e as Error).message).toBe( - `The following error occurred in onMounted: "Error in mounted"` - ); - } + const app = new App(Example, { test: true }); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted"); + 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 // the onWillUnmount hook to be called and to fail expect(mockConsoleError).toBeCalledTimes(1); @@ -448,9 +444,10 @@ describe("errors and promises", () => { root.state = "boom"; root.render(); - await nextTick(); + await expect(nextAppError(root.__owl__.app)).resolves.toThrow( + "error occured in the owl lifecycle" + ); expect(fixture.innerHTML).toBe(""); - expect(mockConsoleError).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1); }); }); @@ -500,13 +497,12 @@ describe("can catch errors", () => { }); } } - let e: Error; - try { - await mount(Root, fixture, { test: true }); - } catch (error) { - e = error as Error; - } - expect(e!.message).toBe( + const app = new App(Root, { test: true }); + let error: OwlError; + const crashProm = expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart"); + await app.mount(fixture).catch((e: Error) => (error = e)); + await crashProm; + expect(error!.message).toBe( `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; - 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); + const app = new App(Root, { test: true }); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted"); + await mountProm; + expect(error!.message).toBe(`The following error occurred in onMounted: "test error"`); + expect(error!.cause).toBe(err); }); test("Errors in owl lifecycle are wrapped in dev mode: async hook", async () => { @@ -546,14 +541,13 @@ describe("can catch errors", () => { }); } } - 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); + const app = new App(Root, { test: true }); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart"); + await mountProm; + expect(error!.message).toBe(`The following error occurred in onWillStart: "test error"`); + expect(error!.cause).toBe(err); }); test("Errors in owl lifecycle are wrapped outside dev mode: sync hook", async () => { @@ -568,16 +562,15 @@ describe("can catch errors", () => { }); } } - let e: OwlError; - try { - await mount(Root, fixture); - } catch (error) { - e = error as OwlError; - } - expect(e!.message).toBe( + const app = new App(Root); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; + expect(error!.message).toBe( `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 () => { @@ -593,16 +586,15 @@ describe("can catch errors", () => { }); } } - let e: OwlError; - try { - await mount(Root, fixture); - } catch (error) { - e = error as OwlError; - } - expect(e!.message).toBe( + const app = new App(Root); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; + expect(error!.message).toBe( `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 () => { @@ -616,16 +608,15 @@ describe("can catch errors", () => { }); } } - let e: OwlError; - try { - await mount(Root, fixture, { test: true }); - } catch (error) { - e = error as OwlError; - } - expect(e!.message).toBe( + const app = new App(Root, { test: true }); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("not an Error was thrown in onMounted"); + await mountProm; + expect(error!.message).toBe( `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 () => { @@ -639,16 +630,15 @@ describe("can catch errors", () => { }); } } - let e: OwlError; - try { - await mount(Root, fixture); - } catch (error) { - e = error as OwlError; - } - expect(e!.message).toBe( + const app = new App(Root); + let error: OwlError; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; + expect(error!.message).toBe( `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 () => { diff --git a/tests/components/hooks.test.ts b/tests/components/hooks.test.ts index a5da81b9..7b72e265 100644 --- a/tests/components/hooks.test.ts +++ b/tests/components/hooks.test.ts @@ -17,8 +17,16 @@ import { useChildSubEnv, useSubEnv, xml, + OwlError, } from "../../src/index"; -import { elem, logStep, makeTestFixture, nextTick, snapshotEverything } from "../helpers"; +import { + elem, + logStep, + makeTestFixture, + nextAppError, + nextTick, + snapshotEverything, +} from "../helpers"; let fixture: HTMLElement; @@ -650,11 +658,12 @@ describe("hooks", () => { } } - try { - await mount(MyComponent, fixture); - } catch (e: any) { - expect(e.cause.message).toBe("Intentional error"); - } + let error: OwlError; + const app = new App(MyComponent); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + 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 expect(console.error).toHaveBeenCalledTimes(0); console.error = originalconsoleError; diff --git a/tests/components/props_validation.test.ts b/tests/components/props_validation.test.ts index e118f408..12f6a894 100644 --- a/tests/components/props_validation.test.ts +++ b/tests/components/props_validation.test.ts @@ -1,6 +1,6 @@ -import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; -import { Component, onError, xml, mount } from "../../src"; -import { DEV_MSG } from "../../src/runtime/app"; +import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers"; +import { Component, onError, xml, mount, OwlError } from "../../src"; +import { App, DEV_MSG } from "../../src/runtime/app"; import { validateProps } from "../../src/runtime/template_helpers"; import { Schema } from "../../src/runtime/validation"; @@ -48,13 +48,14 @@ describe("props validation", () => { static components = { SubComp }; static template = xml`
`; } - let error: Error | undefined; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + const app = new App(Parent, { test: true }); + let error: OwlError | undefined; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow( + "Invalid props for component 'SubComp': 'message' is missing" + ); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing"); error = undefined; @@ -77,12 +78,13 @@ describe("props validation", () => { static template = xml`
`; } - let error: Error; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + const app = new App(Parent, { test: true }); + let error: OwlError | undefined; + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow( + "Invalid props for component 'SubComp': 'message' is missing" + ); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing"); }); @@ -126,14 +128,12 @@ describe("props validation", () => { }; (Parent as any).components = { SubComp }; - let error: Error | undefined; props = {}; - - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + let app = new App(Parent, { test: true }); + let error: OwlError | undefined; + let mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( `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(); props = { p: test.ko }; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + app = new App(Parent, { test: true }); + mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( `Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}` @@ -181,13 +180,12 @@ describe("props validation", () => { static template = xml`
hey
`; }; (Parent as any).components = { SubComp }; - let error: Error | undefined; props = {}; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + let app = new App(Parent, { test: true }); + let error: OwlError | undefined; + let mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( `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(); props = { p: test.ko }; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + app = new App(Parent, { test: true }); + mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( `Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}` @@ -227,26 +224,25 @@ describe("props validation", () => { } let error: Error; let props: { p?: any }; + props = { p: "string" }; try { - props = { p: "string" }; await mount(Parent, fixture, { dev: true }); } catch (e) { error = e as Error; } expect(error!).toBeUndefined(); + props = { p: true }; try { - props = { p: true }; await mount(Parent, fixture, { dev: true }); } catch (e) { error = e as Error; } expect(error!).toBeUndefined(); - try { - props = { p: 1 }; - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + props = { p: 1 }; + const app = new App(Parent, { test: true }); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( "Invalid props for component 'SubComp': 'p' is not a string or boolean" @@ -267,26 +263,25 @@ describe("props validation", () => { } let error: Error; let props: { p?: any }; + props = { p: "key" }; try { - props = { p: "key" }; await mount(Parent, fixture, { dev: true }); } catch (e) { error = e as Error; } expect(error!).toBeUndefined(); + props = {}; try { - props = {}; await mount(Parent, fixture, { dev: true }); } catch (e) { error = e as Error; } expect(error!).toBeUndefined(); - try { - props = { p: 1 }; - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + props = { p: 1 }; + const app = new App(Parent, { test: true }); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; expect(error!).toBeDefined(); 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; } expect(error!).toBeUndefined(); - try { - props = { p: [1] }; - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + props = { p: [1] }; + let app = new App(Parent, { test: true }); + let mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; expect(error!).toBeDefined(); error = undefined; - try { - props = { p: ["string", 1] }; - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + app = new App(Parent, { test: true }); + mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; + expect(error!).toBeDefined(); }); test("can validate an array with multiple sub element types", async () => { @@ -370,12 +363,11 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeUndefined(); - try { - props = { p: [true, 1] }; - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + props = { p: [true, 1] }; + const app = new App(Parent, { test: true }); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( "Invalid props for component 'SubComp': 'p[1]' is not a string or boolean" @@ -405,33 +397,30 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeUndefined(); - try { - props = { p: { id: 1, url: "url", extra: true } }; - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + props = { p: { id: 1, url: "url", extra: true } }; + let app = new App(Parent, { test: true }); + let mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( "Invalid props for component 'SubComp': 'p' has not the correct shape (unknown key 'extra')" ); - try { - props = { p: { id: "1", url: "url" } }; - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + props = { p: { id: "1", url: "url" } }; + app = new App(Parent, { test: true }); + mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( "Invalid props for component 'SubComp': 'p' has not the correct shape ('id' is not a number)" ); error = undefined; - try { - props = { p: { id: 1 } }; - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + props = { p: { id: 1 } }; + app = new App(Parent, { test: true }); + mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( "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; } expect(error!).toBeUndefined(); - try { - props = { p: { id: 1, url: [12, true] } }; - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + props = { p: { id: 1, url: [12, true] } }; + const app = new App(Parent, { test: true }); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( "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 }; } let error: Error; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + const app = new App(Parent, { test: true }); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is missing"); }); @@ -754,11 +741,10 @@ describe("props validation", () => { static template = xml`
`; } let error: Error; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + const app = new App(Parent, { test: true }); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'Child'"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( "Invalid props for component 'Child': 'mandatory' is missing (should be a number)" @@ -859,11 +845,12 @@ describe("default props", () => { static template = xml``; } let error: Error; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + const app = new App(Parent, { test: true }); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow( + "default value cannot be defined for a mandatory prop" + ); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( "A default value cannot be defined for a mandatory prop (name: 'mandatory', component: Child)" diff --git a/tests/components/refs.test.ts b/tests/components/refs.test.ts index f5cb3e1f..a68b0b87 100644 --- a/tests/components/refs.test.ts +++ b/tests/components/refs.test.ts @@ -1,5 +1,5 @@ -import { Component, mount, onMounted, useRef, useState } from "../../src/index"; -import { logStep, makeTestFixture, nextTick, snapshotEverything } from "../helpers"; +import { App, Component, mount, onMounted, useRef, useState } from "../../src/index"; +import { logStep, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers"; import { xml } from "../../src/index"; snapshotEverything(); @@ -94,9 +94,14 @@ describe("refs", () => { ref = useRef("coucou"); } - await expect(async () => { - await mount(Test, fixture); - }).rejects.toThrowError("Cannot have 2 elements with same ref name at the same time"); + const app = new App(Test, { test: true }); + const mountProm = expect(app.mount(fixture)).rejects.toThrowError( + "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); console.warn = consoleWarn; }); diff --git a/tests/components/slots.test.ts b/tests/components/slots.test.ts index be1e574d..9373bf2d 100644 --- a/tests/components/slots.test.ts +++ b/tests/components/slots.test.ts @@ -1,5 +1,5 @@ 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(); let originalconsoleWarn = console.warn; @@ -204,13 +204,12 @@ describe("slots", () => { static components = { Child }; } - let error = null; - try { - await mount(Parent, fixture); - } catch (e) { - error = e; - } - expect(error).not.toBeNull(); + let error: Error; + const app = new App(Parent); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; + expect(error!).not.toBeNull(); expect(mockConsoleWarn).toBeCalledTimes(1); }); diff --git a/tests/components/style_class.test.ts b/tests/components/style_class.test.ts index 4b4d9bc9..d2d0db5f 100644 --- a/tests/components/style_class.test.ts +++ b/tests/components/style_class.test.ts @@ -1,6 +1,6 @@ import { OwlError } from "../../src/runtime/error_handling"; -import { Component, mount, onMounted, useState, xml } from "../../src"; -import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; +import { App, Component, mount, onMounted, useState, xml } from "../../src"; +import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers"; snapshotEverything(); let fixture: HTMLElement; @@ -343,16 +343,15 @@ describe("style and class handling", () => { class Child extends Component { static template = xml`
`; } - class ParentWidget extends Component { + class Parent extends Component { static template = xml``; static components = { Child }; } let error: OwlError; - try { - await mount(ParentWidget, fixture); - } catch (e) { - error = e as OwlError; - } + const app = new App(Parent); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; expect(error!).toBeDefined(); expect(error!.cause).toBeDefined(); const regexp = diff --git a/tests/components/t_foreach.test.ts b/tests/components/t_foreach.test.ts index 155472db..197a516b 100644 --- a/tests/components/t_foreach.test.ts +++ b/tests/components/t_foreach.test.ts @@ -1,5 +1,11 @@ -import { Component, mount, onMounted, useState, xml } from "../../src/index"; -import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers"; +import { App, Component, mount, onMounted, useState, xml } from "../../src/index"; +import { + makeTestFixture, + nextAppError, + nextTick, + snapshotEverything, + useLogLifecycle, +} from "../helpers"; snapshotEverything(); @@ -315,9 +321,13 @@ describe("list of components", () => { `; static components = { Child }; } - await expect(async () => { - await mount(Parent, fixture, { dev: true }); - }).rejects.toThrowError("Got duplicate key in t-foreach: child"); + + const app = new App(Parent, { test: true }); + 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; expect(mockConsoleWarn).toBeCalledTimes(1); }); diff --git a/tests/helpers.ts b/tests/helpers.ts index 714653a7..abdd1ca8 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -261,6 +261,20 @@ expect.extend({ }, }); +export function nextAppError(app: any) { + const { handleError } = app; + return new Promise((resolve) => { + app.handleError = (...args: Parameters) => { + try { + handleError.call(app, ...args); + } catch (e: any) { + app.handleError = handleError; + resolve(e); + } + }; + }); +} + declare global { namespace jest { interface Matchers { diff --git a/tests/misc/portal.test.ts b/tests/misc/portal.test.ts index ff2dcf3e..103aa669 100644 --- a/tests/misc/portal.test.ts +++ b/tests/misc/portal.test.ts @@ -12,7 +12,7 @@ import { } from "../../src"; import { xml } from "../../src/"; 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 originalconsoleWarn = console.warn; @@ -269,11 +269,10 @@ describe("Portal", () => { } let error: Error; - try { - await mount(Parent, fixture); - } catch (e) { - error = e as Error; - } + const app = new App(Parent); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("invalid portal target"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe("invalid portal target"); @@ -960,11 +959,10 @@ describe("Portal: Props validation", () => {
`; } let error: OwlError; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as OwlError; - } + const app = new App(Parent); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle"); + await mountProm; expect(error!).toBeDefined(); expect(error!.cause).toBeDefined(); expect(error!.cause.message).toBe(`' ' is not a valid selector`); @@ -980,11 +978,10 @@ describe("Portal: Props validation", () => {
`; } let error: Error; - try { - await mount(Parent, fixture, { dev: true }); - } catch (e) { - error = e as Error; - } + const app = new App(Parent); + const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); + await expect(nextAppError(app)).resolves.toThrow("invalid portal target"); + await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe(`invalid portal target`); });