Files
owl/tests/components/error_handling.test.ts
T
Lucas Perais (lpe) fb7d25ba0e [FIX] error_handling: onError works if handling happens between culprit and root
Have a component implementing onError that calls a callback from one of its parent.
That parent should not be the original source of the rendering (a parent above is).

Make the callback handle the error and trigger an render()

Before this commit, the parent did not see it was handling a subtree in error,
and did not have a chance to revert that error state in its rendering stack.

After this commit, this flow works.
2025-10-02 12:46:55 +02:00

1914 lines
56 KiB
TypeScript

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`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class Parent extends Component {
static template = xml`<div><ErrorComponent flag="state.flag"/></div>`;
static components = { ErrorComponent };
state = { flag: false };
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><div>heyfalse</div></div>");
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`<SomeMispelledComponent />`;
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`<SomeMispelledComponent />`;
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`<SomeComponent />`;
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`<div><MissingChild /></div>`;
}
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`<div t-att-class="a b">test</div>`;
}
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(\`<div block-attribute-0="class">test</div>\`);
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`<div t-att-class="a b">test</div>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child/>`;
}
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(\`<div block-attribute-0="class">test</div>\`);
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`<div t-esc="a.b.c"/>`;
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Boom />
</t>
</div>`;
static components = { Boom };
error: any = false;
setup() {
onError((err) => {
this.error = err;
this.render();
});
}
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>Error</div>");
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("render from above on error -- handler is not a Root or MountFiber", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
setup() {
onError((err) => {
this.props.onError(err);
});
}
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Boom onError.bind="handleError"/>
</t>
</div>`;
static components = { Boom };
error: any = false;
handleError(err: Error) {
this.error = err;
this.render();
}
}
class GrandParent extends Component {
static template: string = xml`<Parent />`;
static components = { Parent };
}
await mount(GrandParent, fixture);
expect(fixture.innerHTML).toBe("<div>Error</div>");
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`<div><t t-esc="this.will.crash"/></div>`;
}
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`<div>abc</div>`;
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`<div>abc</div>`;
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`<div>abc</div>`;
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`<div t-att-class="{ 'invalid: 5 }">abc</div>`;
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`<div>abc</div>`;
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`<div><t t-esc="val"/></div>`;
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`<div><t t-esc="val"/></div>`;
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`<div><t t-esc="this.will.crash"/></div>`;
}
class Parent extends Component {
static template = xml`<div><Child/></div>`;
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`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
flag = false;
setup() {
onError(({ cause }) => (error = cause));
}
}
const root = await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
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`<span></span>`;
}
class Parent extends Component {
static template = xml`<div><Child/><t t-esc="x.y"/></div>`;
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`<div/>`;
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`<div t-esc="state.a.b"/>`;
state: any = { a: { b: 1 } };
}
const root = await mount(Example, fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
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`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
setup() {
onError(() => (this.state.error = true));
}
}
class App extends Component {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent flag="state.flag"/></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div><div>heyfalse</div></div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("can catch an error in onmounted", async () => {
class ErrorComponent extends Component {
static template = xml`<div>Error!!!</div>`;
setup() {
useLogLifecycle();
onMounted(() => {
throw new Error("error");
});
}
}
class PerfectComponent extends Component {
static template = xml`<div>perfect</div>`;
setup() {
useLogLifecycle();
}
}
class Main extends Component {
static template = xml`Main<t t-if="state.ok" t-component="component"/>`;
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<div>Error!!!</div>");
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<div>perfect</div>");
});
test("calling a hook outside setup should crash", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
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`<t t-esc="state.value"/>`;
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`<t t-esc="state.value"/>`;
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`<t t-esc="state.value"/>`;
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`<t t-esc="state.value"/>`;
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`<t t-esc="state.value"/>`;
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`<t t-esc="state.value"/>`;
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`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
setup() {
onError(() => {
this.state.error = true;
});
}
}
class App extends Component {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent };
}
await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
setup() {
onError(() => (this.state.error = true));
}
}
class App extends Component {
static template = xml`
<div>
<ErrorBoundary t-if="state.flag"><ErrorComponent /></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = await mount(App, fixture);
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>Some text</div>`;
setup() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component {
static template = xml`<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
setup() {
onError(() => (this.state.error = true));
}
}
class App extends Component {
static template = xml`<div>
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent };
}
await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>classic</div>`;
}
class ErrorComponent extends Component {
static template = xml`<div>Some text</div>`;
setup() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component {
static template = xml`<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
setup() {
onError(() => (this.state.error = true));
}
}
class App extends Component {
static template = xml`<div>
<ErrorBoundary><ClassicCompoent/><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent, ClassicCompoent };
}
await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>Some text</div>`;
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`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
setup() {
onError(() => (this.state.error = true));
}
}
class App extends Component {
static template = xml`<div><ErrorBoundary><ErrorComponent /></ErrorBoundary></div>`;
static components = { ErrorBoundary, ErrorComponent };
}
await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>classic</div>`;
}
class ErrorComponent extends Component {
static template = xml`<div>Some text</div>`;
setup() {
onWillStart(() => {
throw new Error("NOOOOO");
});
}
}
class ErrorBoundary extends Component {
static template = xml`<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
setup() {
onError(() => (this.state.error = true));
}
}
class App extends Component {
static template = xml`<div>
<ErrorBoundary><ClassicCompoent/><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent, ClassicCompoent };
}
await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>Some text</div>`;
setup() {
useLogLifecycle();
onMounted(() => {
logStep("boom");
throw new Error("NOOOOO");
});
}
}
class ErrorBoundary extends Component {
static template = xml`<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
setup() {
useLogLifecycle();
onError(() => (this.state.error = true));
}
}
class Root extends Component {
static template = xml`<div>
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent };
setup() {
useLogLifecycle();
}
}
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>Some text</div>`;
setup() {
useLogLifecycle();
onMounted(() => {
logStep("boom");
throw new Error("NOOOOO");
});
}
}
class Root extends Component {
static template = xml`<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><ErrorComponent /></t>
</div>`;
static components = { ErrorComponent };
state = useState({ error: false });
setup() {
useLogLifecycle();
onError(() => (this.state.error = true));
}
}
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div>Error handled</div>");
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`<div>Some text</div>`;
setup() {
useLogLifecycle();
onMounted(() => {
logStep("boom");
throw new Error("NOOOOO");
});
}
}
class C extends Component {
static template = xml`<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><Boom/></t>
</div>`;
static components = { Boom };
state = useState({ error: false });
setup() {
useLogLifecycle();
onError(() => (this.state.error = true));
}
}
class B extends Component {
static template = xml`<div><C/></div>`;
static components = { C };
setup() {
useLogLifecycle();
}
}
class A extends Component {
static template = xml`<B/>`;
static components = { B };
setup() {
useLogLifecycle();
}
}
await mount(A, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>Some text</div>`;
setup() {
useLogLifecycle();
onMounted(() => {
logStep("boom");
throw new Error("NOOOOO");
});
}
}
class ErrorBoundary extends Component {
static template = xml`<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
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`<div>
<OK/>
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent, OK };
setup() {
useLogLifecycle();
}
}
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div>OK<div>Error handled</div></div>");
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`<div><t t-esc="props.message"/></div>`;
setup() {
onWillPatch(() => {
throw new Error("NOOOOO");
});
}
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
setup() {
onError(() => (this.state.error = true));
}
}
class App extends Component {
static template = xml`
<div>
<span><t t-esc="state.message"/></span>
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
</div>`;
state = useState({ message: "abc" });
static components = { ErrorBoundary, ErrorComponent };
}
const app = await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><span>abc</span><div><div>abc</div></div></div>");
app.state.message = "def";
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>def</span><div>Error handled</div></div>");
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("catchError in catchError", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
}
class Child extends Component {
static template = xml`
<div>
<Boom />
</div>`;
static components = { Boom };
setup() {
onError((error) => {
throw error;
});
}
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Child />
</t>
</div>`;
static components = { Child };
error: any = false;
setup() {
onError((error) => {
this.error = error;
this.render();
});
}
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>Error</div>");
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`<div>
<t t-if="!state.error">
<t t-esc="this.will.crash" />
</t>
<t t-else="">
<t t-esc="state.error"/>
</t>
</div>`;
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`<Concrete />`;
}
await mount(Parent, fixture);
expect(steps).toStrictEqual(["Concrete onError"]);
expect(fixture.innerHTML).toBe("<div>Concrete</div>");
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("onError in class inheritance is called if rethrown", async () => {
const steps: string[] = [];
class Abstract extends Component {
static template = xml`<div>
<t t-if="!state.error">
<t t-esc="this.will.crash" />
</t>
<t t-else="">
<t t-esc="state.error"/>
</t>
</div>`;
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`<Concrete />`;
}
await mount(Parent, fixture);
expect(steps).toStrictEqual(["Concrete onError", "Abstract onError"]);
expect(fixture.innerHTML).toBe("<div>Abstract</div>");
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`<div />`;
setup() {
throw new Error("My Error");
}
}
class Child extends Component {
static template = xml`<ErrorComponent />`;
static components = { ErrorComponent };
setup() {
onError((error) => {
throw error;
});
}
}
class Sibling extends Component {
static template = xml`<div>Sibling</div>`;
}
class ErrorHandler extends Component {
static template = xml`<t t-slot="default" />`;
setup() {
onError(() => {
this.props.onError();
Promise.resolve().then(() => {
parentState.cps[2] = {
id: 2,
Comp: Sibling,
};
});
});
}
}
class Parent extends Component {
static template = xml`
<t t-foreach="Object.values(state.cps)" t-as="cp" t-key="cp.id">
<ErrorHandler onError="() => this.cleanUp(cp.id)">
<t t-component="cp.Comp" />
</ErrorHandler>
</t>`;
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("<div>Sibling</div>");
});
test("catching in child makes parent render", async () => {
class Child extends Component {
static template = xml`<div t-esc="'Child ' + props.id" />`;
}
class ErrorComp extends Component {
static template = xml`<div />`;
setup() {
throw new Error("Error Component");
}
}
class Catch extends Component {
static template = xml`<t t-slot="default" />`;
setup() {
onError(({ cause }) => {
this.props.onError(cause);
});
}
}
const steps: any[] = [];
class Parent extends Component {
static components = { Catch };
static template = xml`
<t t-foreach="Object.entries(this.elements)" t-as="elem" t-key="elem[0]">
<Catch onError="(error) => this.onError(elem[0], error)">
<t t-component="elem[1]" id="elem[0]" />
</Catch>
</t>
`;
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("<div>Child 2</div>");
expect(steps).toEqual(["Error Component"]);
});
test("an error in onWillDestroy", async () => {
class Child extends Component {
static template = xml`<div>abc</div>`;
setup() {
useLogLifecycle();
onWillDestroy(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<t t-if="state.hasChild"><Child/></t>`;
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<div>abc</div>");
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`<div>abc</div>`;
setup() {
useLogLifecycle();
onWillDestroy(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<t t-if="state.hasChild"><Child/></t>`;
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<Child/><Boom/>`;
static components = { Child, Boom };
setup() {
useLogLifecycle();
}
}
class Root extends Component {
static template = xml`<t t-component="component"/>`;
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<Child/><Boom/>`;
static components = { Child, Boom };
setup() {
useLogLifecycle();
}
}
class Root extends Component {
static template = xml`R<t t-if="state.gogogo" t-component="component"/>`;
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",
]
`);
});
});