diff --git a/tests/components/error_handling.test.ts b/tests/components/error_handling.test.ts index 151287c1..67254427 100644 --- a/tests/components/error_handling.test.ts +++ b/tests/components/error_handling.test.ts @@ -1,1875 +1,1875 @@ -// 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, -// steps, -// } from "../helpers"; -// import { OwlError } from "../../src/common/owl_error"; - -// 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 'ctx' - -// 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 'ctx' - -// 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("can catch an error in onmounted", async () => { -// class ErrorComponent extends Component { -// static template = xml`
Error!!!
`; -// setup() { -// useLogLifecycle(); -// onMounted(() => { -// throw new Error("error"); -// }); -// } -// } -// class PerfectComponent extends Component { -// static template = xml`
perfect
`; -// setup() { -// useLogLifecycle(); -// } -// } -// class Main extends Component { -// static template = xml`Main`; -// component: any; -// state: any; -// setup() { -// this.state = useState({ ok: false }); -// useLogLifecycle(); -// this.component = ErrorComponent; -// onError(() => { -// this.component = PerfectComponent; -// this.render(); -// }); -// } -// } - -// const app = await mount(Main, fixture); -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Main:setup", -// "Main:willStart", -// "Main:willRender", -// "Main:rendered", -// "Main:mounted", -// ] -// `); -// expect(fixture.innerHTML).toBe("Main"); -// (app as any).state.ok = true; -// await nextTick(); -// expect(fixture.innerHTML).toBe("Main
Error!!!
"); -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Main:willRender", -// "ErrorComponent:setup", -// "ErrorComponent:willStart", -// "Main:rendered", -// "ErrorComponent:willRender", -// "ErrorComponent:rendered", -// "Main:willPatch", -// "ErrorComponent:mounted", -// "Main:willRender", -// "PerfectComponent:setup", -// "PerfectComponent:willStart", -// "Main:rendered", -// ] -// `); -// await nextTick(); -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "PerfectComponent:willRender", -// "PerfectComponent:rendered", -// "Main:willPatch", -// "ErrorComponent:willUnmount", -// "ErrorComponent:willDestroy", -// "PerfectComponent:mounted", -// "Main:patched", -// ] -// `); -// expect(fixture.innerHTML).toBe("Main
perfect
"); -// }); - -// 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(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "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", -// ] -// `); -// 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(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "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", -// ] -// `); -// 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(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "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", -// ] -// `); -// 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(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "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", -// ] -// `); -// 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(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Parent:setup", -// "Parent:willStart", -// "Parent:willRender", -// "Child:setup", -// "Child:willStart", -// "Parent:rendered", -// "Child:willRender", -// "Child:rendered", -// "Child:mounted", -// "Parent:mounted", -// ] -// `); -// parent.state.hasChild = false; -// await nextTick(); -// await nextTick(); -// await nextTick(); -// await nextTick(); -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Parent:willRender", -// "Parent:rendered", -// "Parent:willPatch", -// "Child:willUnmount", -// "Child:willDestroy", -// "Parent:patched", -// "Parent:willRender", -// "Parent:rendered", -// "Parent:willPatch", -// "Parent:patched", -// ] -// `); -// 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(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Parent:setup", -// "Parent:willStart", -// "Parent:willRender", -// "Parent:rendered", -// "Parent:mounted", -// ] -// `); - -// parent.state.hasChild = true; -// await nextMicroTick(); -// await nextMicroTick(); -// await nextMicroTick(); -// await nextMicroTick(); -// await nextMicroTick(); -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Parent:willRender", -// "Child:setup", -// "Child:willStart", -// "Parent:rendered", -// "Child:willRender", -// "Child:rendered", -// ] -// `); -// parent.state.hasChild = false; -// await nextTick(); -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Parent:willRender", -// "Parent:rendered", -// "Child:willDestroy", -// "Parent:willRender", -// "Parent:rendered", -// ] -// `); -// expect(fixture.innerHTML).toBe("1"); -// await nextTick(); -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Parent:willPatch", -// "Parent:patched", -// ] -// `); -// expect(fixture.innerHTML).toBe("2"); -// }); - -// test("error in onMounted, graceful recovery", async () => { -// class Child extends Component { -// static template = xml`abc`; -// setup() { -// useLogLifecycle(); -// } -// } - -// class OtherChild extends Component { -// static template = xml`def`; -// setup() { -// useLogLifecycle(); -// } -// } - -// class Boom extends Component { -// static template = xml`boom`; -// setup() { -// useLogLifecycle(); -// onMounted(() => { -// throw new Error("boom"); -// }); -// } -// } - -// class Parent extends Component { -// static template = xml`parent`; -// static components = { Child, Boom }; -// setup() { -// useLogLifecycle(); -// } -// } - -// class Root extends Component { -// static template = xml``; - -// component: any = Parent; -// setup() { -// useLogLifecycle(); -// onError(() => { -// logStep("error"); -// this.component = OtherChild; -// this.render(); -// }); -// } -// } - -// await mount(Root, fixture); -// expect(fixture.innerHTML).toBe("def"); - -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Root:setup", -// "Root:willStart", -// "Root:willRender", -// "Parent:setup", -// "Parent:willStart", -// "Root:rendered", -// "Parent:willRender", -// "Child:setup", -// "Child:willStart", -// "Boom:setup", -// "Boom:willStart", -// "Parent:rendered", -// "Child:willRender", -// "Child:rendered", -// "Boom:willRender", -// "Boom:rendered", -// "Boom:mounted", -// "error", -// "Root:willRender", -// "OtherChild:setup", -// "OtherChild:willStart", -// "Root:rendered", -// "OtherChild:willRender", -// "OtherChild:rendered", -// "OtherChild:mounted", -// "Root:mounted", -// ] -// `); -// }); - -// test("error in onMounted, graceful recovery, variation", async () => { -// class Child extends Component { -// static template = xml`abc`; -// setup() { -// useLogLifecycle(); -// } -// } - -// class OtherChild extends Component { -// static template = xml`def`; -// setup() { -// useLogLifecycle(); -// } -// } - -// class Boom extends Component { -// static template = xml`boom`; -// setup() { -// useLogLifecycle(); -// onMounted(() => { -// throw new Error("boom"); -// }); -// } -// } - -// class Parent extends Component { -// static template = xml`parent`; -// static components = { Child, Boom }; -// setup() { -// useLogLifecycle(); -// } -// } - -// class Root extends Component { -// static template = xml`R`; - -// component: any = Parent; -// state = useState({ gogogo: false }); - -// setup() { -// useLogLifecycle(); -// onError(() => { -// logStep("error"); -// this.component = OtherChild; -// this.render(); -// }); -// } -// } - -// const root = await mount(Root, fixture); -// expect(fixture.innerHTML).toBe("R"); - -// // standard mounting process -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Root:setup", -// "Root:willStart", -// "Root:willRender", -// "Root:rendered", -// "Root:mounted", -// ] -// `); - -// root.state.gogogo = true; -// await nextTick(); - -// expect(fixture.innerHTML).toBe("Rparentabcboom"); -// // rerender, root creates sub components, it crashes, tries to recover -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "Root:willRender", -// "Parent:setup", -// "Parent:willStart", -// "Root:rendered", -// "Parent:willRender", -// "Child:setup", -// "Child:willStart", -// "Boom:setup", -// "Boom:willStart", -// "Parent:rendered", -// "Child:willRender", -// "Child:rendered", -// "Boom:willRender", -// "Boom:rendered", -// "Root:willPatch", -// "Boom:mounted", -// "error", -// "Root:willRender", -// "OtherChild:setup", -// "OtherChild:willStart", -// "Root:rendered", -// ] -// `); - -// await nextTick(); -// expect(fixture.innerHTML).toBe("Rdef"); - -// expect(steps.splice(0)).toMatchInlineSnapshot(` -// Array [ -// "OtherChild:willRender", -// "OtherChild:rendered", -// "Root:willPatch", -// "Child:willDestroy", -// "Boom:willUnmount", -// "Boom:willDestroy", -// "Parent:willDestroy", -// "OtherChild:mounted", -// "Root:patched", -// ] -// `); -// }); -// }); +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, + steps, +} from "../helpers"; +import { OwlError } from "../../src/common/owl_error"; + +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 'ctx' + +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 'ctx' + +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("can catch an error in onmounted", async () => { + class ErrorComponent extends Component { + static template = xml`
Error!!!
`; + setup() { + useLogLifecycle(); + onMounted(() => { + throw new Error("error"); + }); + } + } + class PerfectComponent extends Component { + static template = xml`
perfect
`; + setup() { + useLogLifecycle(); + } + } + class Main extends Component { + static template = xml`Main`; + component: any; + state: any; + setup() { + this.state = useState({ ok: false }); + useLogLifecycle(); + this.component = ErrorComponent; + onError(() => { + this.component = PerfectComponent; + this.render(); + }); + } + } + + const app = await mount(Main, fixture); + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Main:setup", + "Main:willStart", + "Main:willRender", + "Main:rendered", + "Main:mounted", + ] + `); + expect(fixture.innerHTML).toBe("Main"); + (app as any).state.ok = true; + await nextTick(); + expect(fixture.innerHTML).toBe("Main
Error!!!
"); + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Main:willRender", + "ErrorComponent:setup", + "ErrorComponent:willStart", + "Main:rendered", + "ErrorComponent:willRender", + "ErrorComponent:rendered", + "Main:willPatch", + "ErrorComponent:mounted", + "Main:willRender", + "PerfectComponent:setup", + "PerfectComponent:willStart", + "Main:rendered", + ] + `); + await nextTick(); + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "PerfectComponent:willRender", + "PerfectComponent:rendered", + "Main:willPatch", + "ErrorComponent:willUnmount", + "ErrorComponent:willDestroy", + "PerfectComponent:mounted", + "Main:patched", + ] + `); + expect(fixture.innerHTML).toBe("Main
perfect
"); + }); + + 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(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "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", + ] + `); + 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(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "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", + ] + `); + 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(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "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", + ] + `); + 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(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "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", + ] + `); + 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(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Parent:setup", + "Parent:willStart", + "Parent:willRender", + "Child:setup", + "Child:willStart", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + "Child:mounted", + "Parent:mounted", + ] + `); + parent.state.hasChild = false; + await nextTick(); + await nextTick(); + await nextTick(); + await nextTick(); + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Parent:willRender", + "Parent:rendered", + "Parent:willPatch", + "Child:willUnmount", + "Child:willDestroy", + "Parent:patched", + "Parent:willRender", + "Parent:rendered", + "Parent:willPatch", + "Parent:patched", + ] + `); + 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(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Parent:setup", + "Parent:willStart", + "Parent:willRender", + "Parent:rendered", + "Parent:mounted", + ] + `); + + parent.state.hasChild = true; + await nextMicroTick(); + await nextMicroTick(); + await nextMicroTick(); + await nextMicroTick(); + await nextMicroTick(); + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Parent:willRender", + "Child:setup", + "Child:willStart", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + ] + `); + parent.state.hasChild = false; + await nextTick(); + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Parent:willRender", + "Parent:rendered", + "Child:willDestroy", + "Parent:willRender", + "Parent:rendered", + ] + `); + expect(fixture.innerHTML).toBe("1"); + await nextTick(); + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Parent:willPatch", + "Parent:patched", + ] + `); + expect(fixture.innerHTML).toBe("2"); + }); + + test("error in onMounted, graceful recovery", async () => { + class Child extends Component { + static template = xml`abc`; + setup() { + useLogLifecycle(); + } + } + + class OtherChild extends Component { + static template = xml`def`; + setup() { + useLogLifecycle(); + } + } + + class Boom extends Component { + static template = xml`boom`; + setup() { + useLogLifecycle(); + onMounted(() => { + throw new Error("boom"); + }); + } + } + + class Parent extends Component { + static template = xml`parent`; + static components = { Child, Boom }; + setup() { + useLogLifecycle(); + } + } + + class Root extends Component { + static template = xml``; + + component: any = Parent; + setup() { + useLogLifecycle(); + onError(() => { + logStep("error"); + this.component = OtherChild; + this.render(); + }); + } + } + + await mount(Root, fixture); + expect(fixture.innerHTML).toBe("def"); + + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Root:setup", + "Root:willStart", + "Root:willRender", + "Parent:setup", + "Parent:willStart", + "Root:rendered", + "Parent:willRender", + "Child:setup", + "Child:willStart", + "Boom:setup", + "Boom:willStart", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + "Boom:willRender", + "Boom:rendered", + "Boom:mounted", + "error", + "Root:willRender", + "OtherChild:setup", + "OtherChild:willStart", + "Root:rendered", + "OtherChild:willRender", + "OtherChild:rendered", + "OtherChild:mounted", + "Root:mounted", + ] + `); + }); + + test("error in onMounted, graceful recovery, variation", async () => { + class Child extends Component { + static template = xml`abc`; + setup() { + useLogLifecycle(); + } + } + + class OtherChild extends Component { + static template = xml`def`; + setup() { + useLogLifecycle(); + } + } + + class Boom extends Component { + static template = xml`boom`; + setup() { + useLogLifecycle(); + onMounted(() => { + throw new Error("boom"); + }); + } + } + + class Parent extends Component { + static template = xml`parent`; + static components = { Child, Boom }; + setup() { + useLogLifecycle(); + } + } + + class Root extends Component { + static template = xml`R`; + + component: any = Parent; + state = useState({ gogogo: false }); + + setup() { + useLogLifecycle(); + onError(() => { + logStep("error"); + this.component = OtherChild; + this.render(); + }); + } + } + + const root = await mount(Root, fixture); + expect(fixture.innerHTML).toBe("R"); + + // standard mounting process + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Root:setup", + "Root:willStart", + "Root:willRender", + "Root:rendered", + "Root:mounted", + ] + `); + + root.state.gogogo = true; + await nextTick(); + + expect(fixture.innerHTML).toBe("Rparentabcboom"); + // rerender, root creates sub components, it crashes, tries to recover + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Root:willRender", + "Parent:setup", + "Parent:willStart", + "Root:rendered", + "Parent:willRender", + "Child:setup", + "Child:willStart", + "Boom:setup", + "Boom:willStart", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + "Boom:willRender", + "Boom:rendered", + "Root:willPatch", + "Boom:mounted", + "error", + "Root:willRender", + "OtherChild:setup", + "OtherChild:willStart", + "Root:rendered", + ] + `); + + await nextTick(); + expect(fixture.innerHTML).toBe("Rdef"); + + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "OtherChild:willRender", + "OtherChild:rendered", + "Root:willPatch", + "Child:willDestroy", + "Boom:willUnmount", + "Boom:willDestroy", + "Parent:willDestroy", + "OtherChild:mounted", + "Root:patched", + ] + `); + }); +});