import { App, Component, mount, onWillDestroy } from "../../src"; import { onError, onMounted, onPatched, onWillPatch, onWillStart, onWillRender, onRendered, onWillUnmount, useState, xml, } from "../../src/index"; import { logStep, makeTestFixture, nextTick, nextMicroTick, snapshotEverything, useLogLifecycle, nextAppError, } from "../helpers"; import { OwlError } from "../../src/runtime/error_handling"; let fixture: HTMLElement; snapshotEverything(); let originalconsoleError = console.error; let mockConsoleError: any; let originalconsoleWarn = console.warn; let mockConsoleWarn: any; beforeEach(() => { fixture = makeTestFixture(); mockConsoleError = jest.fn(() => {}); mockConsoleWarn = jest.fn(() => {}); console.error = mockConsoleError; console.warn = mockConsoleWarn; }); afterEach(() => { console.error = originalconsoleError; console.warn = originalconsoleWarn; }); describe("basics", () => { test("no component catching error lead to full app destruction", async () => { class ErrorComponent extends Component { static template = xml`
hey
`; } class Parent extends Component { static template = xml`
`; static components = { ErrorComponent }; state = { flag: false }; } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
heyfalse
"); parent.state.flag = true; parent.render(); await expect(nextAppError(parent.__owl__.app)).resolves.toThrow( "An error occured in the owl lifecycle" ); expect(fixture.innerHTML).toBe(""); expect(mockConsoleWarn).toBeCalledTimes(1); }); test("display a nice error if it cannot find component", async () => { class SomeComponent extends Component {} class Parent extends Component { static template = xml``; static components = { SomeComponent }; } const app = new App(Parent); let error: 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); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(1); }); test("display a nice error if it cannot find component (in dev mode)", async () => { class SomeComponent extends Component {} class Parent extends Component { static template = xml``; static components = { SomeComponent }; } const app = new App(Parent, { test: true }); let error: 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); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(1); }); test("display a nice error if a component is not a component", async () => { function notAComponentConstructor() {} class Parent extends Component { static template = xml``; static components = { SomeComponent: notAComponentConstructor }; } const app = new App(Parent as typeof Component); let error: 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' ); }); test("display a nice error if the components key is missing with subcomponents", async () => { class Parent extends Component { static template = xml`
`; } const app = new App(Parent as typeof Component); let error: Error; const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); await expect(nextAppError(app)).resolves.toThrow( 'Cannot find the definition of component "MissingChild", missing static components key in parent' ); await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe( 'Cannot find the definition of component "MissingChild", missing static components key in parent' ); }); test("display a nice error if the root component template fails to compile", async () => { // This is a special case: mount throws synchronously and we don't have any // node which can handle the error, hence the different structure of this test class Comp extends Component { static template = xml`
test
`; } const app = new App(Comp); let error: Error; try { await app.mount(fixture); } catch (e) { error = e as Error; } const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier generated code: function(app, bdom, helpers) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; let block1 = createBlock(\`
test
\`); return function template(ctx, node, key = "") { let attr1 = ctx['a']ctx['b']; return block1([attr1]); } }`; expect(error!).toBeDefined(); expect(error!.message).toBe(expectedErrorMessage); }); test("display a nice error if a non-root component template fails to compile", async () => { class Child extends Component { static template = xml`
test
`; } class Parent extends Component { static components = { Child }; static template = xml``; } const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier generated code: function(app, bdom, helpers) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; let block1 = createBlock(\`
test
\`); return function template(ctx, node, key = "") { let attr1 = ctx['a']ctx['b']; return block1([attr1]); } }`; const app = new App(Parent as typeof Component); let error: Error; const mountProm = app.mount(fixture).catch((e: Error) => (error = e)); await expect(nextAppError(app)).resolves.toThrow(expectedErrorMessage); await mountProm; expect(error!).toBeDefined(); expect(error!.message).toBe(expectedErrorMessage); }); test("simple catchError", async () => { class Boom extends Component { static template = xml`
`; } class Parent extends Component { static template = xml`
Error
`; static components = { Boom }; error: any = false; setup() { onError((err) => { this.error = err; this.render(); }); } } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
Error
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); }); describe("errors and promises", () => { test("a rendering error will reject the mount promise", async () => { // we do not catch error in willPatch anymore class Root extends Component { static template = xml`
`; } 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!).toBeDefined(); expect(error!.cause).toBeDefined(); const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error!.cause.message).toMatch(regexp); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0); }); test("an error in mounted call will reject the mount promise", async () => { class Root extends Component { static template = xml`
abc
`; setup() { onMounted(() => { throw new Error("boom"); }); } } 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!).toBeDefined(); expect(error!.cause).toBeDefined(); expect(error!.cause.message).toBe("boom"); expect(fixture.innerHTML).toBe(""); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(1); }); test("an error in onMounted callback will have the component's setup in its stack trace", async () => { class Root extends Component { static template = xml`
abc
`; setup() { onMounted(() => { throw new Error("boom"); }); } } 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("Root.setup"); expect(error!.stack).toContain("error_handling.test.ts"); expect(fixture.innerHTML).toBe(""); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(1); }); test("errors in onWillRender/onRender aren't wrapped more than once", async () => { class Root extends Component { static template = xml`
abc
`; setup() { onWillRender(() => { throw new Error("boom in onWillRender"); }); onRendered(() => { throw new Error("boom in onRendered"); }); } } 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"` ); }); test("error while rendering component isn't wrapped by onWillRender/onRendered", async () => { class App extends Component { static template = xml`
abc
`; setup() { onWillRender(() => {}); onRendered(() => {}); } } let error: Error; try { await mount(App, fixture, { test: true }); } catch (e) { error = e as Error; } expect(error!).toBeDefined(); expect(error!.message).toBe("Tokenizer error: could not tokenize `{ 'invalid: 5 }`"); }); test("wrapped errors in async code are correctly caught", async () => { class Root extends Component { static template = xml`
abc
`; setup() { onWillStart(async () => { await Promise.resolve(); throw new Error("boom in onWillStart"); }); } } 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"` ); await new Promise((r) => setTimeout(r, 0)); // wait for the rejection event to bubble }); test("an error in willPatch call will reject the render promise", async () => { class Root extends Component { static template = xml`
`; val = 3; setup() { onWillPatch(() => { throw new Error("boom"); }); onError((e) => (error = e)); } } const root = await mount(Root, fixture, { test: true }); root.val = 4; let error: Error; root.render(); await nextTick(); expect(error!).toBeDefined(); expect(error!.message).toBe(`The following error occurred in onWillPatch: "boom"`); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("an error in patched call will reject the render promise", async () => { class Root extends Component { static template = xml`
`; val = 3; setup() { onPatched(() => { throw new Error("boom"); }); onError((e) => (error = e)); } } const root = await mount(Root, fixture, { test: true }); root.val = 4; let error: Error; root.render(); await nextTick(); expect(error!).toBeDefined(); expect(error!.message).toBe(`The following error occurred in onPatched: "boom"`); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("a rendering error in a sub component will reject the mount promise", async () => { // we do not catch error in willPatch anymore class Child extends Component { static template = xml`
`; } class Parent extends Component { static template = xml`
`; static components = { Child }; } const app = new App(Parent); 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!).toBeDefined(); expect(error!.cause).toBeDefined(); const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error!.cause.message).toMatch(regexp); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(1); }); test("a rendering error will reject the render promise", async () => { class Root extends Component { static template = xml`
`; flag = false; setup() { onError(({ cause }) => (error = cause)); } } const root = await mount(Root, fixture); expect(fixture.innerHTML).toBe("
"); root.flag = true; let error: Error; root.render(); await nextTick(); expect(error!).toBeDefined(); const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error!.message).toMatch(regexp); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("a rendering error will reject the render promise (with sub components)", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; } const app = new App(Parent); 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!).toBeDefined(); expect(error!.cause).toBeDefined(); const regexp = /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g; expect(error!.cause.message).toMatch(regexp); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(1); }); test("errors in mounted and in willUnmount", async () => { class Example extends Component { static template = xml`
`; val: any; setup() { onMounted(() => { throw new Error("Error in mounted"); this.val = { foo: "bar" }; }); onWillUnmount(() => { console.log(this.val.foo); }); } } 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); expect(mockConsoleWarn).toBeCalledTimes(1); }); test("errors in rerender", async () => { class Example extends Component { static template = xml`
`; state: any = { a: { b: 1 } }; } const root = await mount(Example, fixture); expect(fixture.innerHTML).toBe("
1
"); root.state = "boom"; root.render(); await expect(nextAppError(root.__owl__.app)).resolves.toThrow( "error occured in the owl lifecycle" ); expect(fixture.innerHTML).toBe(""); expect(mockConsoleWarn).toBeCalledTimes(1); }); }); describe("can catch errors", () => { test("can catch an error in a component render function", async () => { class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; state = useState({ flag: false }); static components = { ErrorBoundary, ErrorComponent }; } const app = await mount(App, fixture); expect(fixture.innerHTML).toBe("
heyfalse
"); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("calling a hook outside setup should crash", async () => { class Root extends Component { static template = xml``; state = useState({ value: 1 }); setup() { onWillStart(() => { this.state = useState({ value: 2 }); }); } } 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')"` ); }); test("Errors have the right cause", async () => { const err = new Error("test error"); class Root extends Component { static template = xml``; state = useState({ value: 1 }); setup() { onMounted(() => { throw 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 () => { const err = new Error("test error"); class Root extends Component { static template = xml``; state = useState({ value: 1 }); setup() { onWillStart(async () => { await nextMicroTick(); throw 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 () => { const err = new Error("test error"); class Root extends Component { static template = xml``; state = useState({ value: 1 }); setup() { onMounted(() => { throw err; }); } } 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(error!.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``; state = useState({ value: 1 }); setup() { onWillStart(async () => { await nextMicroTick(); throw err; }); } } 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(error!.cause).toBe(err); }); test("Thrown values that are not errors are wrapped in dev mode", async () => { class Root extends Component { static template = xml``; state = useState({ value: 1 }); setup() { onMounted(() => { throw "This is not an 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("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(error!.cause).toBe("This is not an error"); }); test("Thrown values that are not errors are wrapped outside dev mode", async () => { class Root extends Component { static template = xml``; state = useState({ value: 1 }); setup() { onMounted(() => { throw "This is not an error"; }); } } 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(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 () => { class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => { this.state.error = true; }); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the initial call of a component render function (parent updated)", async () => { class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; state = useState({ flag: false }); static components = { ErrorBoundary, ErrorComponent }; } const app = await mount(App, fixture); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the constructor call of a component render function", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the constructor call of a component render function 2", async () => { class ClassicCompoent extends Component { static template = xml`
classic
`; } class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent, ClassicCompoent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the willStart call", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { onWillStart(async () => { // we wait a little bit to be in a different stack frame await nextTick(); throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error origination from a child's willStart function", async () => { class ClassicCompoent extends Component { static template = xml`
classic
`; } class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { onWillStart(() => { throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent, ClassicCompoent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the mounted call", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { useLogLifecycle(); onMounted(() => { logStep("boom"); throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { useLogLifecycle(); onError(() => (this.state.error = true)); } } class Root extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; setup() { useLogLifecycle(); } } await mount(Root, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect([ "Root:setup", "Root:willStart", "Root:willRender", "ErrorBoundary:setup", "ErrorBoundary:willStart", "Root:rendered", "ErrorBoundary:willRender", "ErrorComponent:setup", "ErrorComponent:willStart", "ErrorBoundary:rendered", "ErrorComponent:willRender", "ErrorComponent:rendered", "ErrorComponent:mounted", "boom", "ErrorBoundary:willRender", "ErrorBoundary:rendered", "ErrorBoundary:mounted", "Root:mounted", ]).toBeLogged(); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the mounted call (in root component)", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { useLogLifecycle(); onMounted(() => { logStep("boom"); throw new Error("NOOOOO"); }); } } class Root extends Component { static template = xml`
Error handled
`; static components = { ErrorComponent }; state = useState({ error: false }); setup() { useLogLifecycle(); onError(() => (this.state.error = true)); } } await mount(Root, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect([ "Root:setup", "Root:willStart", "Root:willRender", "ErrorComponent:setup", "ErrorComponent:willStart", "Root:rendered", "ErrorComponent:willRender", "ErrorComponent:rendered", "ErrorComponent:mounted", "boom", "Root:willRender", "Root:rendered", "Root:mounted", ]).toBeLogged(); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the mounted call (in child of child)", async () => { class Boom extends Component { static template = xml`
Some text
`; setup() { useLogLifecycle(); onMounted(() => { logStep("boom"); throw new Error("NOOOOO"); }); } } class C extends Component { static template = xml`
Error handled
`; static components = { Boom }; state = useState({ error: false }); setup() { useLogLifecycle(); onError(() => (this.state.error = true)); } } class B extends Component { static template = xml`
`; static components = { C }; setup() { useLogLifecycle(); } } class A extends Component { static template = xml``; static components = { B }; setup() { useLogLifecycle(); } } await mount(A, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect([ "A:setup", "A:willStart", "A:willRender", "B:setup", "B:willStart", "A:rendered", "B:willRender", "C:setup", "C:willStart", "B:rendered", "C:willRender", "Boom:setup", "Boom:willStart", "C:rendered", "Boom:willRender", "Boom:rendered", "Boom:mounted", "boom", "C:willRender", "C:rendered", "C:mounted", "B:mounted", "A:mounted", ]).toBeLogged(); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("error in mounted on a component with a sibling (properly mounted)", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { useLogLifecycle(); onMounted(() => { logStep("boom"); throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { useLogLifecycle(); onError(() => (this.state.error = true)); } } class OK extends Component { static template = xml`OK`; setup() { useLogLifecycle(); } } class Root extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent, OK }; setup() { useLogLifecycle(); } } await mount(Root, fixture); expect(fixture.innerHTML).toBe("
OK
Error handled
"); expect([ "Root:setup", "Root:willStart", "Root:willRender", "OK:setup", "OK:willStart", "ErrorBoundary:setup", "ErrorBoundary:willStart", "Root:rendered", "OK:willRender", "OK:rendered", "ErrorBoundary:willRender", "ErrorComponent:setup", "ErrorComponent:willStart", "ErrorBoundary:rendered", "ErrorComponent:willRender", "ErrorComponent:rendered", "ErrorComponent:mounted", "boom", "ErrorBoundary:willRender", "ErrorBoundary:rendered", "ErrorBoundary:mounted", "OK:mounted", "Root:mounted", ]).toBeLogged(); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the willPatch call", async () => { class ErrorComponent extends Component { static template = xml`
`; setup() { onWillPatch(() => { throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; state = useState({ message: "abc" }); static components = { ErrorBoundary, ErrorComponent }; } const app = await mount(App, fixture); expect(fixture.innerHTML).toBe("
abc
abc
"); app.state.message = "def"; await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
def
Error handled
"); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("catchError in catchError", async () => { class Boom extends Component { static template = xml`
`; } class Child extends Component { static template = xml`
`; static components = { Boom }; setup() { onError((error) => { throw error; }); } } class Parent extends Component { static template = xml`
Error
`; static components = { Child }; error: any = false; setup() { onError((error) => { this.error = error; this.render(); }); } } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
Error
"); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("onError in class inheritance is not called if no rethrown", async () => { const steps: string[] = []; class Abstract extends Component { static template = xml`
`; state: any; setup() { this.state = useState({}); onError(() => { steps.push("Abstract onError"); this.state.error = "Abstract"; }); } } class Concrete extends Abstract { setup() { super.setup(); onError(() => { steps.push("Concrete onError"); this.state.error = "Concrete"; }); } } class Parent extends Component { static components = { Concrete }; static template = xml``; } await mount(Parent, fixture); expect(steps).toStrictEqual(["Concrete onError"]); expect(fixture.innerHTML).toBe("
Concrete
"); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("onError in class inheritance is called if rethrown", async () => { const steps: string[] = []; class Abstract extends Component { static template = xml`
`; state: any; setup() { this.state = useState({}); onError(() => { steps.push("Abstract onError"); this.state.error = "Abstract"; }); } } class Concrete extends Abstract { setup() { super.setup(); onError((error) => { steps.push("Concrete onError"); this.state.error = "Concrete"; throw error; }); } } class Parent extends Component { static components = { Concrete }; static template = xml``; } await mount(Parent, fixture); expect(steps).toStrictEqual(["Concrete onError", "Abstract onError"]); expect(fixture.innerHTML).toBe("
Abstract
"); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("catching error, rethrow, render parent -- a main component loop implementation", async () => { let parentState: any; class ErrorComponent extends Component { static template = xml`
`; setup() { throw new Error("My Error"); } } class Child extends Component { static template = xml``; static components = { ErrorComponent }; setup() { onError((error) => { throw error; }); } } class Sibling extends Component { static template = xml`
Sibling
`; } class ErrorHandler extends Component { static template = xml``; setup() { onError(() => { this.props.onError(); Promise.resolve().then(() => { parentState.cps[2] = { id: 2, Comp: Sibling, }; }); }); } } class Parent extends Component { static template = xml` `; static components = { ErrorHandler }; state: any = useState({ cps: {}, }); setup() { parentState = this.state; } cleanUp(id: number) { delete this.state.cps[id]; } } await mount(Parent, fixture); parentState.cps[1] = { id: 1, Comp: Child }; await nextMicroTick(); expect(fixture.innerHTML).toBe(""); await nextTick(); expect(fixture.innerHTML).toBe("
Sibling
"); }); test("catching in child makes parent render", async () => { class Child extends Component { static template = xml`
`; } class ErrorComp extends Component { static template = xml`
`; setup() { throw new Error("Error Component"); } } class Catch extends Component { static template = xml``; setup() { onError(({ cause }) => { this.props.onError(cause); }); } } const steps: any[] = []; class Parent extends Component { static components = { Catch }; static template = xml` `; elements: any = {}; onError(id: any, error: Error) { steps.push(error.message); delete this.elements[id]; this.elements[2] = Child; this.render(); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe(""); parent.elements[1] = ErrorComp; parent.render(); await nextTick(); expect(fixture.innerHTML).toBe("
Child 2
"); expect(steps).toEqual(["Error Component"]); }); test("an error in onWillDestroy", async () => { class Child extends Component { static template = xml`
abc
`; setup() { useLogLifecycle(); onWillDestroy(() => { throw new Error("boom"); }); } } class Parent extends Component { static template = xml` `; static components = { Child }; state = useState({ value: 1, hasChild: true }); setup() { useLogLifecycle(); onError(() => { this.state.value++; }); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("1
abc
"); expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Child:setup", "Child:willStart", "Parent:rendered", "Child:willRender", "Child:rendered", "Child:mounted", "Parent:mounted", ]).toBeLogged(); parent.state.hasChild = false; await nextTick(); await nextTick(); await nextTick(); await nextTick(); expect([ "Parent:willRender", "Parent:rendered", "Parent:willPatch", "Child:willUnmount", "Child:willDestroy", "Parent:patched", "Parent:willRender", "Parent:rendered", "Parent:willPatch", "Parent:patched", ]).toBeLogged(); expect(fixture.innerHTML).toBe("2"); }); test("an error in onWillDestroy, variation", async () => { class Child extends Component { static template = xml`
abc
`; setup() { useLogLifecycle(); onWillDestroy(() => { throw new Error("boom"); }); } } class Parent extends Component { static template = xml` `; static components = { Child }; state = useState({ value: 1, hasChild: false }); setup() { useLogLifecycle(); onError(() => { this.state.value++; }); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("1"); expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Parent:rendered", "Parent:mounted", ]).toBeLogged(); parent.state.hasChild = true; await nextMicroTick(); await nextMicroTick(); await nextMicroTick(); await nextMicroTick(); await nextMicroTick(); expect([ "Parent:willRender", "Child:setup", "Child:willStart", "Parent:rendered", "Child:willRender", "Child:rendered", ]).toBeLogged(); parent.state.hasChild = false; await nextTick(); expect([ "Parent:willRender", "Parent:rendered", "Child:willDestroy", "Parent:willRender", "Parent:rendered", ]).toBeLogged(); expect(fixture.innerHTML).toBe("1"); await nextTick(); expect(["Parent:willPatch", "Parent:patched"]).toBeLogged(); expect(fixture.innerHTML).toBe("2"); }); });