mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] component: add catchError hook
This commit is contained in:
+76
-9
@@ -21,6 +21,7 @@
|
||||
- [References](#references)
|
||||
- [Slots](#slots)
|
||||
- [Asynchronous Rendering](#asynchronous-rendering)
|
||||
- [Error Handling](#error-handling)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -188,15 +189,16 @@ A solid and robust component system needs useful hooks/methods to help
|
||||
developers write components. Here is a complete description of the lifecycle of
|
||||
a owl component:
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------------------------ | ----------------------------------------------------- |
|
||||
| **[constructor](#constructorparent-props)** | constructor |
|
||||
| **[willStart](#willstart)** | async, before first rendering |
|
||||
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
|
||||
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
|
||||
| **[willPatch](#willpatch)** | just before the DOM is patched |
|
||||
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
|
||||
| **[willUnmount](#willunmount)** | just before removing component from DOM |
|
||||
| Method | Description |
|
||||
| ------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| **[constructor](#constructorparent-props)** | constructor |
|
||||
| **[willStart](#willstart)** | async, before first rendering |
|
||||
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
|
||||
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
|
||||
| **[willPatch](#willpatch)** | just before the DOM is patched |
|
||||
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
|
||||
| **[willUnmount](#willunmount)** | just before removing component from DOM |
|
||||
| **[catchError](#catcherrorerror)** | catch errors (see [error handling section](#error-handling)) |
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -337,6 +339,12 @@ the DOM. This is a good place to remove some listeners, for example.
|
||||
|
||||
This is the opposite method of `mounted`.
|
||||
|
||||
#### `catchError(error)`
|
||||
|
||||
The `catchError` method is useful when we need to intercept and properly react
|
||||
to (rendering) errors that occur in some sub components. See the section on
|
||||
[error handling](#error-handling)
|
||||
|
||||
### Root Component
|
||||
|
||||
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
|
||||
@@ -1020,3 +1028,62 @@ Here are a few tips on how to work with asynchronous components:
|
||||
<AsyncChild t-asyncroot="1"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
By default, whenever an error occurs in the rendering of an Owl application, we
|
||||
destroy the whole application. Otherwise, we cannot offer any guarantee on the
|
||||
state of the resulting component tree. It might be hopelessly corrupted, but
|
||||
without any user-visible state.
|
||||
|
||||
Clearly, it sometimes is a little bit extreme to destroy the application. This
|
||||
is why we have a builtin mechanism to handle rendering errors (and errors coming
|
||||
from lifecycle hooks): the `catchError` hook.
|
||||
|
||||
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
|
||||
sub components rendering and/or lifecycle method calls will be caught and given
|
||||
to the `catchError` method. This allow us to properly handle the error, and to
|
||||
not break the application.
|
||||
|
||||
For example, here is how we could implement an `ErrorBoundary` component:
|
||||
|
||||
```xml
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">
|
||||
Error handled
|
||||
</t>
|
||||
<t t-else="1">
|
||||
<t t-slot="default" />
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using the `ErrorBoundary` is then extremely simple:
|
||||
|
||||
```xml
|
||||
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
|
||||
```
|
||||
|
||||
Note that we need to be careful here: the fallback UI should not throw any
|
||||
error, otherwise we risk going into an infinite loop.
|
||||
|
||||
Also, it may be useful to know that whenever an error is caught, it is then
|
||||
broadcasted to the application by an event on the `qweb` instance. It may be
|
||||
useful, for example, to log the error somewhere.
|
||||
|
||||
```js
|
||||
env.qweb.on("error", null, function(error) {
|
||||
// do something
|
||||
// react to the error
|
||||
});
|
||||
```
|
||||
|
||||
+83
-25
@@ -243,6 +243,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
*/
|
||||
willUnmount() {}
|
||||
|
||||
/**
|
||||
* catchError is a method called whenever some error happens in the rendering or
|
||||
* lifecycle hooks of a child.
|
||||
*/
|
||||
catchError(error: Error): void {}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -398,7 +404,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
for (let key in handlers) {
|
||||
handlers[key]();
|
||||
}
|
||||
this.mounted();
|
||||
try {
|
||||
this.mounted();
|
||||
} catch (e) {
|
||||
errorHandler(e, this);
|
||||
}
|
||||
}
|
||||
|
||||
__callWillUnmount() {
|
||||
@@ -419,7 +429,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
forceUpdate: boolean = false,
|
||||
patchQueue?: any[],
|
||||
scope?: any,
|
||||
vars?: any,
|
||||
vars?: any
|
||||
): Promise<void> {
|
||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||
if (shouldUpdate) {
|
||||
@@ -451,7 +461,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
}
|
||||
|
||||
async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
|
||||
await this.willStart();
|
||||
try {
|
||||
await this.willStart();
|
||||
} catch (e) {
|
||||
errorHandler(e, this);
|
||||
return Promise.resolve(h("div"));
|
||||
}
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.isDestroyed) {
|
||||
return Promise.resolve(h("div"));
|
||||
@@ -485,7 +500,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
return this.__render(false, [], scope, vars);
|
||||
}
|
||||
|
||||
async __render(force: boolean = false, patchQueue: any[] = [], scope?: Object, vars?: any): Promise<VNode> {
|
||||
async __render(
|
||||
force: boolean = false,
|
||||
patchQueue: any[] = [],
|
||||
scope?: Object,
|
||||
vars?: any
|
||||
): Promise<VNode> {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderId++;
|
||||
const promises: Promise<void>[] = [];
|
||||
@@ -496,15 +516,21 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = false;
|
||||
}
|
||||
let vnode = __owl__.render!(this, {
|
||||
promises,
|
||||
handlers: __owl__.boundHandlers,
|
||||
mountedHandlers: __owl__.mountedHandlers,
|
||||
forceUpdate: force,
|
||||
patchQueue,
|
||||
scope,
|
||||
vars
|
||||
});
|
||||
let vnode;
|
||||
try {
|
||||
vnode = __owl__.render!(this, {
|
||||
promises,
|
||||
handlers: __owl__.boundHandlers,
|
||||
mountedHandlers: __owl__.mountedHandlers,
|
||||
forceUpdate: force,
|
||||
patchQueue,
|
||||
scope,
|
||||
vars
|
||||
});
|
||||
} catch (e) {
|
||||
vnode = __owl__.vnode || h("div");
|
||||
errorHandler(e, this);
|
||||
}
|
||||
patch.push(vnode);
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = true;
|
||||
@@ -580,18 +606,50 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
* 3) Call 'patched' on the component of each patch, in inverse order
|
||||
*/
|
||||
__applyPatchQueue(patchQueue: any[]) {
|
||||
const patchLen = patchQueue.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue[i];
|
||||
patch.push(patch[0].willPatch());
|
||||
}
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue[i];
|
||||
patch[0].__patch(patch[1]);
|
||||
}
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
const patch = patchQueue[i];
|
||||
patch[0].patched(patch[2]);
|
||||
let component = this;
|
||||
try {
|
||||
const patchLen = patchQueue.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue[i];
|
||||
component = patch[0];
|
||||
patch.push(patch[0].willPatch());
|
||||
}
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue[i];
|
||||
patch[0].__patch(patch[1]);
|
||||
}
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
const patch = patchQueue[i];
|
||||
component = patch[0];
|
||||
patch[0].patched(patch[2]);
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler(e, component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Error handling
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function errorHandler(error, component) {
|
||||
let canCatch = false;
|
||||
let qweb = component.env.qweb;
|
||||
let root = component;
|
||||
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
|
||||
root = component;
|
||||
component = component.__owl__.parent;
|
||||
}
|
||||
console.error(error);
|
||||
// we trigger error on QWeb so it can be logged/handled
|
||||
qweb.trigger("error", error);
|
||||
|
||||
if (canCatch) {
|
||||
setTimeout(() => {
|
||||
component.catchError(error);
|
||||
});
|
||||
} else {
|
||||
root.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
+309
-15
@@ -879,18 +879,22 @@ describe("composition", () => {
|
||||
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
||||
});
|
||||
|
||||
test("throw a nice error if it cannot find component", async () => {
|
||||
expect.assertions(1);
|
||||
test("display a nice error if it cannot find component", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
||||
env.qweb.addTemplate("Parent", `<div><SomeMispelledWidget /></div>`);
|
||||
class Parent extends Widget {
|
||||
components = { SomeWidget: Widget };
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
try {
|
||||
await parent.mount(fixture);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Cannot find the definition of component "SomeMispelledWidget"');
|
||||
}
|
||||
await parent.mount(fixture);
|
||||
|
||||
expect(console.error).toBeCalledTimes(1);
|
||||
expect((<any>console.error).mock.calls[0][0].message).toMatch(
|
||||
'Cannot find the definition of component "SomeMispelledWidget"'
|
||||
);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test("t-refs on widget are components", async () => {
|
||||
@@ -2726,7 +2730,8 @@ describe("widget and observable state", () => {
|
||||
});
|
||||
|
||||
test("subcomponents cannot change observable state received from parent", async () => {
|
||||
expect.assertions(1);
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
env.qweb.addTemplate("Parent", `<div><Child obj="state.obj"/></div>`);
|
||||
class Parent extends Widget {
|
||||
state = { obj: { coffee: 1 } };
|
||||
@@ -2739,11 +2744,13 @@ describe("widget and observable state", () => {
|
||||
}
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
try {
|
||||
await parent.mount(fixture);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Observed state cannot be changed here! (key: "coffee", val: "2")');
|
||||
}
|
||||
await parent.mount(fixture);
|
||||
|
||||
expect(console.error).toBeCalledTimes(1);
|
||||
expect((<any>console.error).mock.calls[0][0].message).toMatch(
|
||||
'Observed state cannot be changed here! (key: "coffee", val: "2")'
|
||||
);
|
||||
console.error = consoleError;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3260,10 +3267,9 @@ describe("t-slot directive", () => {
|
||||
await app.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div><button>Inc[4]</button><div><div> SC:4</div></div></div>");
|
||||
(<any>fixture.querySelector('button')).click();
|
||||
(<any>fixture.querySelector("button")).click();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><button>Inc[5]</button><div><div> SC:5</div></div></div>");
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3525,3 +3531,291 @@ describe("environment and plugins", () => {
|
||||
expect(fixture.innerHTML).toBe("<div>Blue</div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("component error handling (catchError)", () => {
|
||||
/**
|
||||
* This test suite requires often to wait for 3 ticks. Here is why:
|
||||
* - First tick is to let the app render and crash.
|
||||
* - When we crash, we call the catchError handler in a setTimeout (because we
|
||||
* need to wait for the previous rendering to be completely stopped). So, we
|
||||
* need to wait for the second tick.
|
||||
* - Then, when the handler changes the state, we need to wait for the interface
|
||||
* to be rerendered.
|
||||
* */
|
||||
|
||||
test("can catch an error in a component render function", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else="1"><t t-slot="default" /></t>
|
||||
</div>
|
||||
<div t-name="ErrorComponent">hey<t t-esc="props.flag and state.this.will.crash"/>
|
||||
</div>
|
||||
<div t-name="App">
|
||||
<ErrorBoundary><ErrorComponent flag="state.flag"/></ErrorBoundary>
|
||||
</div>
|
||||
</templates>`);
|
||||
const handler = jest.fn();
|
||||
env.qweb.on("error", null, handler);
|
||||
class ErrorComponent extends Widget {}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Widget {
|
||||
state = { flag: false };
|
||||
components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div><div>hey</div></div></div>");
|
||||
app.state.flag = true;
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
|
||||
expect(console.error).toBeCalledTimes(1);
|
||||
console.error = consoleError;
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("no component catching error lead to full app destruction", async () => {
|
||||
const handler = jest.fn();
|
||||
env.qweb.on("error", null, handler);
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ErrorComponent">hey<t t-esc="props.flag and state.this.will.crash"/>
|
||||
</div>
|
||||
<div t-name="App">
|
||||
<ErrorComponent flag="state.flag"/>
|
||||
</div>
|
||||
</templates>`);
|
||||
class ErrorComponent extends Widget {}
|
||||
class App extends Widget {
|
||||
state = { flag: false };
|
||||
components = { ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
|
||||
app.state.flag = true;
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
|
||||
expect(console.error).toBeCalledTimes(1);
|
||||
console.error = consoleError;
|
||||
expect(app.__owl__.isDestroyed).toBe(true);
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("can catch an error in the initial call of a component render function", async () => {
|
||||
const handler = jest.fn();
|
||||
env.qweb.on("error", null, handler);
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else="1"><t t-slot="default" /></t>
|
||||
</div>
|
||||
<div t-name="ErrorComponent">hey<t t-esc="state.this.will.crash"/>
|
||||
</div>
|
||||
<div t-name="App">
|
||||
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
|
||||
</div>
|
||||
</templates>`);
|
||||
class ErrorComponent extends Widget {}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Widget {
|
||||
components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
|
||||
expect(console.error).toBeCalledTimes(1);
|
||||
console.error = consoleError;
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("can catch an error in the constructor call of a component render function", async () => {
|
||||
const handler = jest.fn();
|
||||
env.qweb.on("error", null, handler);
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else="1"><t t-slot="default" /></t>
|
||||
</div>
|
||||
<div t-name="ErrorComponent">Some text</div>
|
||||
<div t-name="App">
|
||||
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
|
||||
</div>
|
||||
</templates>`);
|
||||
class ErrorComponent extends Widget {
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
throw new Error("NOOOOO");
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Widget {
|
||||
components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
|
||||
expect(console.error).toBeCalledTimes(1);
|
||||
console.error = consoleError;
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("can catch an error in the willStart call", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else="1"><t t-slot="default" /></t>
|
||||
</div>
|
||||
<div t-name="ErrorComponent">Some text</div>
|
||||
<div t-name="App">
|
||||
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
|
||||
</div>
|
||||
</templates>`);
|
||||
class ErrorComponent extends Widget {
|
||||
async willStart() {
|
||||
// we wait a little bit to be in a different stack frame
|
||||
await nextTick();
|
||||
throw new Error("NOOOOO");
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Widget {
|
||||
components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
|
||||
expect(console.error).toBeCalledTimes(1);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test("can catch an error in the mounted call", async () => {
|
||||
console.error = jest.fn();
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else="1"><t t-slot="default" /></t>
|
||||
</div>
|
||||
<div t-name="ErrorComponent">Some text</div>
|
||||
<div t-name="App">
|
||||
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
|
||||
</div>
|
||||
</templates>`);
|
||||
class ErrorComponent extends Widget {
|
||||
mounted() {
|
||||
throw new Error("NOOOOO");
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Widget {
|
||||
components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
});
|
||||
|
||||
test("can catch an error in the willPatch call", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else="1"><t t-slot="default" /></t>
|
||||
</div>
|
||||
<div t-name="ErrorComponent"><t t-esc="props.message"/></div>
|
||||
<div t-name="App">
|
||||
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
|
||||
</div>
|
||||
</templates>`);
|
||||
class ErrorComponent extends Widget {
|
||||
willPatch() {
|
||||
throw new Error("NOOOOO");
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Widget {
|
||||
state = { message: "abc" };
|
||||
components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div><div>abc</div></div></div>");
|
||||
app.state.message = "def";
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user