mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
968e96ad08
If a component onMounted hook causes an error, and an error handler tries to reinitiate a new correct render, the fiber may be recycled. But the scheduler removes it from its own set of tasks, so it will be ignored. We simply check for this condition before removing it from the tasks set.
1682 lines
50 KiB
TypeScript
1682 lines
50 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);
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|