import { 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, } from "../helpers"; 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 nextTick(); expect(fixture.innerHTML).toBe(""); expect(mockConsoleError).toBeCalledTimes(1); 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 }; } let error: Error; try { await mount(Parent, fixture); } catch (e) { error = e as Error; } 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 }; } let error: Error; try { await mount(Parent, fixture, { test: true }); } catch (e) { error = e as Error; } 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("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 App extends Component { static template = xml`
`; } let error: Error; try { await mount(App, fixture); } catch (e) { error = e as Error; } 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(mockConsoleError).toBeCalledTimes(0); }); test("an error in mounted call will reject the mount promise", async () => { class App extends Component { static template = xml`
abc
`; setup() { onMounted(() => { throw new Error("boom"); }); } } let error: Error; try { await mount(App, fixture); } catch (e) { error = e as Error; } expect(error!).toBeDefined(); expect(error!.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 App extends Component { static template = xml`
abc
`; setup() { onMounted(() => { throw new Error("boom"); }); } } let error: Error; try { await mount(App, fixture, { test: true }); } catch (e) { error = e as Error; } expect(error!).toBeDefined(); expect(error!.stack).toContain("App.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 App extends Component { static template = xml`
abc
`; setup() { onWillRender(() => { throw new Error("boom in onWillRender"); }); onRendered(() => { throw new Error("boom in onRendered"); }); } } let error: Error; try { await mount(App, fixture, { test: true }); } catch (e) { error = e as Error; } 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("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 App extends Component { static template = xml`
`; static components = { Child }; } let error: Error; try { await mount(App, fixture); } catch (e) { error = e as Error; } 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(1); }); test("a rendering error will reject the render promise", async () => { class Root extends Component { static template = xml`
`; flag = false; setup() { onError((e) => (error = e)); } } 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 }; } let error: Error; try { await mount(Parent, fixture); } catch (e) { error = e as Error; } expect(error!).toBeDefined(); const regexp = /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g; expect(error!.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); }); } } try { await mount(Example, fixture, { test: true }); } catch (e) { expect((e as 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 nextTick(); expect(fixture.innerHTML).toBe(""); expect(mockConsoleError).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1); }); }); describe("can catch errors", () => { test("can catch an error in a component render function", async () => { class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; state = useState({ flag: false }); static components = { ErrorBoundary, ErrorComponent }; } const app = await mount(App, fixture); expect(fixture.innerHTML).toBe("
heyfalse
"); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("calling a hook outside setup should crash", async () => { class Root extends Component { static template = xml``; state = useState({ value: 1 }); setup() { onWillStart(() => { this.state = useState({ value: 2 }); }); } } let e: Error; try { await mount(Root, fixture, { test: true }); } catch (error) { e = error as Error; } expect(e!.message).toBe( `The following error occurred in onWillStart: "No active component (a hook function should only be called in 'setup')"` ); }); test("can catch an error in the initial call of a component render function (parent mounted)", async () => { class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => { this.state.error = true; }); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the initial call of a component render function (parent updated)", async () => { class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; state = useState({ flag: false }); static components = { ErrorBoundary, ErrorComponent }; } const app = await mount(App, fixture); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the constructor call of a component render function", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the constructor call of a component render function 2", async () => { class ClassicCompoent extends Component { static template = xml`
classic
`; } class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent, ClassicCompoent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the willStart call", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { onWillStart(async () => { // we wait a little bit to be in a different stack frame await nextTick(); throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error origination from a child's willStart function", async () => { class ClassicCompoent extends Component { static template = xml`
classic
`; } class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { onWillStart(() => { throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent, ClassicCompoent }; } await mount(App, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the mounted call", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { useLogLifecycle(); onMounted(() => { logStep("boom"); throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { useLogLifecycle(); onError(() => (this.state.error = true)); } } class Root extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; setup() { useLogLifecycle(); } } await mount(Root, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect([ "Root:setup", "Root:willStart", "Root:willRender", "ErrorBoundary:setup", "ErrorBoundary:willStart", "Root:rendered", "ErrorBoundary:willRender", "ErrorComponent:setup", "ErrorComponent:willStart", "ErrorBoundary:rendered", "ErrorComponent:willRender", "ErrorComponent:rendered", "ErrorComponent:mounted", "boom", "ErrorBoundary:willRender", "ErrorBoundary:rendered", "ErrorBoundary:mounted", "Root:mounted", ]).toBeLogged(); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the mounted call (in root component)", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { useLogLifecycle(); onMounted(() => { logStep("boom"); throw new Error("NOOOOO"); }); } } class Root extends Component { static template = xml`
Error handled
`; static components = { ErrorComponent }; state = useState({ error: false }); setup() { useLogLifecycle(); onError(() => (this.state.error = true)); } } await mount(Root, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect([ "Root:setup", "Root:willStart", "Root:willRender", "ErrorComponent:setup", "ErrorComponent:willStart", "Root:rendered", "ErrorComponent:willRender", "ErrorComponent:rendered", "ErrorComponent:mounted", "boom", "Root:willRender", "Root:rendered", "Root:mounted", ]).toBeLogged(); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the mounted call (in child of child)", async () => { class Boom extends Component { static template = xml`
Some text
`; setup() { useLogLifecycle(); onMounted(() => { logStep("boom"); throw new Error("NOOOOO"); }); } } class C extends Component { static template = xml`
Error handled
`; static components = { Boom }; state = useState({ error: false }); setup() { useLogLifecycle(); onError(() => (this.state.error = true)); } } class B extends Component { static template = xml`
`; static components = { C }; setup() { useLogLifecycle(); } } class A extends Component { static template = xml``; static components = { B }; setup() { useLogLifecycle(); } } await mount(A, fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect([ "A:setup", "A:willStart", "A:willRender", "B:setup", "B:willStart", "A:rendered", "B:willRender", "C:setup", "C:willStart", "B:rendered", "C:willRender", "Boom:setup", "Boom:willStart", "C:rendered", "Boom:willRender", "Boom:rendered", "Boom:mounted", "boom", "C:willRender", "C:rendered", "C:mounted", "B:mounted", "A:mounted", ]).toBeLogged(); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("error in mounted on a component with a sibling (properly mounted)", async () => { class ErrorComponent extends Component { static template = xml`
Some text
`; setup() { useLogLifecycle(); onMounted(() => { logStep("boom"); throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { useLogLifecycle(); onError(() => (this.state.error = true)); } } class OK extends Component { static template = xml`OK`; setup() { useLogLifecycle(); } } class Root extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent, OK }; setup() { useLogLifecycle(); } } await mount(Root, fixture); expect(fixture.innerHTML).toBe("
OK
Error handled
"); expect([ "Root:setup", "Root:willStart", "Root:willRender", "OK:setup", "OK:willStart", "ErrorBoundary:setup", "ErrorBoundary:willStart", "Root:rendered", "OK:willRender", "OK:rendered", "ErrorBoundary:willRender", "ErrorComponent:setup", "ErrorComponent:willStart", "ErrorBoundary:rendered", "ErrorComponent:willRender", "ErrorComponent:rendered", "ErrorComponent:mounted", "boom", "ErrorBoundary:willRender", "ErrorBoundary:rendered", "ErrorBoundary:mounted", "OK:mounted", "Root:mounted", ]).toBeLogged(); expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("can catch an error in the willPatch call", async () => { class ErrorComponent extends Component { static template = xml`
`; setup() { onWillPatch(() => { throw new Error("NOOOOO"); }); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); setup() { onError(() => (this.state.error = true)); } } class App extends Component { static template = xml`
`; state = useState({ message: "abc" }); static components = { ErrorBoundary, ErrorComponent }; } const app = await mount(App, fixture); expect(fixture.innerHTML).toBe("
abc
abc
"); app.state.message = "def"; await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
def
Error handled
"); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("catchError in catchError", async () => { class Boom extends Component { static template = xml`
`; } class Child extends Component { static template = xml`
`; static components = { Boom }; setup() { onError((error) => { throw error; }); } } class Parent extends Component { static template = xml`
Error
`; static components = { Child }; error: any = false; setup() { onError((error) => { this.error = error; this.render(); }); } } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
Error
"); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("onError in class inheritance is not called if no rethrown", async () => { const steps: string[] = []; class Abstract extends Component { static template = xml`
`; state: any; setup() { this.state = useState({}); onError(() => { steps.push("Abstract onError"); this.state.error = "Abstract"; }); } } class Concrete extends Abstract { setup() { super.setup(); onError(() => { steps.push("Concrete onError"); this.state.error = "Concrete"; }); } } class Parent extends Component { static components = { Concrete }; static template = xml``; } await mount(Parent, fixture); expect(steps).toStrictEqual(["Concrete onError"]); expect(fixture.innerHTML).toBe("
Concrete
"); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("onError in class inheritance is called if rethrown", async () => { const steps: string[] = []; class Abstract extends Component { static template = xml`
`; state: any; setup() { this.state = useState({}); onError(() => { steps.push("Abstract onError"); this.state.error = "Abstract"; }); } } class Concrete extends Abstract { setup() { super.setup(); onError((error) => { steps.push("Concrete onError"); this.state.error = "Concrete"; throw error; }); } } class Parent extends Component { static components = { Concrete }; static template = xml``; } await mount(Parent, fixture); expect(steps).toStrictEqual(["Concrete onError", "Abstract onError"]); expect(fixture.innerHTML).toBe("
Abstract
"); expect(mockConsoleWarn).toBeCalledTimes(0); }); test("catching error, rethrow, render parent -- a main component loop implementation", async () => { let parentState: any; class ErrorComponent extends Component { static template = xml`
`; setup() { throw new Error("My Error"); } } class Child extends Component { static template = xml``; static components = { ErrorComponent }; setup() { onError((error) => { throw error; }); } } class Sibling extends Component { static template = xml`
Sibling
`; } class ErrorHandler extends Component { static template = xml``; setup() { onError(() => { this.props.onError(); Promise.resolve().then(() => { parentState.cps[2] = { id: 2, Comp: Sibling, }; }); }); } } class Parent extends Component { static template = xml` `; static components = { ErrorHandler }; state: any = useState({ cps: {}, }); setup() { parentState = this.state; } cleanUp(id: number) { delete this.state.cps[id]; } } await mount(Parent, fixture); parentState.cps[1] = { id: 1, Comp: Child }; await nextMicroTick(); expect(fixture.innerHTML).toBe(""); await nextTick(); expect(fixture.innerHTML).toBe("
Sibling
"); }); test("catching in child makes parent render", async () => { class Child extends Component { static template = xml`
`; } class ErrorComp extends Component { static template = xml`
`; setup() { throw new Error("Error Component"); } } class Catch extends Component { static template = xml``; setup() { onError((error) => { this.props.onError(error); }); } } const steps: any[] = []; class Parent extends Component { static components = { Catch }; static template = xml` `; elements: any = {}; onError(id: any, error: Error) { steps.push(error.message); delete this.elements[id]; this.elements[2] = Child; this.render(); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe(""); parent.elements[1] = ErrorComp; parent.render(); await nextTick(); expect(fixture.innerHTML).toBe("
Child 2
"); expect(steps).toEqual(["Error Component"]); }); test("an error in onWillDestroy", async () => { class Child extends Component { static template = xml`
abc
`; setup() { useLogLifecycle(); onWillDestroy(() => { throw new Error("boom"); }); } } class Parent extends Component { static template = xml` `; static components = { Child }; state = useState({ value: 1, hasChild: true }); setup() { useLogLifecycle(); onError(() => { this.state.value++; }); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("1
abc
"); expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Child:setup", "Child:willStart", "Parent:rendered", "Child:willRender", "Child:rendered", "Child:mounted", "Parent:mounted", ]).toBeLogged(); parent.state.hasChild = false; await nextTick(); await nextTick(); await nextTick(); await nextTick(); expect([ "Parent:willRender", "Parent:rendered", "Parent:willPatch", "Child:willUnmount", "Child:willDestroy", "Parent:willRender", "Parent:rendered", "Parent:willPatch", "Parent:patched", ]).toBeLogged(); expect(fixture.innerHTML).toBe("2"); }); test("an error in onWillDestroy, variation", async () => { class Child extends Component { static template = xml`
abc
`; setup() { useLogLifecycle(); onWillDestroy(() => { throw new Error("boom"); }); } } class Parent extends Component { static template = xml` `; static components = { Child }; state = useState({ value: 1, hasChild: false }); setup() { useLogLifecycle(); onError(() => { this.state.value++; }); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("1"); expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Parent:rendered", "Parent:mounted", ]).toBeLogged(); parent.state.hasChild = true; await nextMicroTick(); await nextMicroTick(); await nextMicroTick(); await nextMicroTick(); await nextMicroTick(); expect([ "Parent:willRender", "Child:setup", "Child:willStart", "Parent:rendered", "Child:willRender", "Child:rendered", ]).toBeLogged(); parent.state.hasChild = false; await nextTick(); expect([ "Child:willDestroy", "Parent:willRender", "Parent:rendered", "Parent:willPatch", "Parent:patched", ]).toBeLogged(); expect(fixture.innerHTML).toBe("2"); }); });