import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { EventBus } from "../../src/core/event_bus";
import {
makeDeferred,
makeTestFixture,
makeTestEnv,
nextMicroTick,
nextTick,
normalize,
editInput
} from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
env.qweb.addTemplate("Widget", "
");
env.qweb.addTemplate(
"Counter",
`
`
);
env.qweb.addTemplate("WidgetA", `
Hello
`);
env.qweb.addTemplate("WidgetB", `
world
`);
});
afterEach(() => {
fixture.remove();
});
class Widget extends Component {}
function children(w: Widget): Widget[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
// Test components
class Counter extends Widget {
state = useState({
counter: 0
});
inc() {
this.state.counter++;
}
}
class WidgetB extends Widget {}
class WidgetA extends Widget {
static components = { b: WidgetB };
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("basic widget properties", () => {
test("props is properly defined", async () => {
const widget = new Widget(env);
expect(widget.props).toEqual({});
});
test("has no el after creation", async () => {
const widget = new Widget(env);
expect(widget.el).toBe(null);
});
test("can be mounted", async () => {
const widget = new Widget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
});
test("crashes if it cannot find a template", async () => {
expect.assertions(1);
class SomeWidget extends Component {}
try {
new SomeWidget(env);
} catch (e) {
expect(e.message).toBe('Could not find template for component "SomeWidget"');
}
});
test("can be clicked on and updated", async () => {
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("
");
});
test("cannot be clicked on and updated if not in DOM", async () => {
const counter = new Counter(env);
const target = document.createElement("div");
await counter.mount(target);
expect(target.innerHTML).toBe("
`);
});
test("reconciliation alg is not confused in some specific situation", async () => {
// in this test, we set t-key to 4 because it was in conflict with the
// template id corresponding to the first child.
class Child extends Component {
static template = xml`child`;
}
class Parent extends Component {
static template = xml`
`);
class ParentWidget extends Widget {
static components = { child: ChildWidget };
state = useState({ n: 1 });
willPatch() {
steps.push("parent:willPatch");
}
patched() {
steps.push("parent:patched");
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(steps).toEqual([]);
widget.state.n = 2;
await nextTick();
widget.destroy();
expect(steps).toEqual([
"parent:willPatch",
"child:willPatch",
"childchild:willPatch",
"childchild:patched",
"child:patched",
"parent:patched"
]);
});
test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => {
const steps: string[] = [];
// the t-else part in the template is important. This is
// necessary to have a situation that could confuse the vdom
// patching algorithm
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class ChildWidget extends Widget {
async willStart() {
steps.push("child:willStart");
}
mounted() {
steps.push("child:mounted");
}
}
class ParentWidget extends Widget {
state = useState({ ok: false });
static components = { child: ChildWidget };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(steps).toEqual([]);
widget.state.ok = true;
await nextTick();
expect(steps).toEqual(["child:willStart", "child:mounted"]);
});
test("mounted hook is correctly called on subcomponents created in mounted hook", async done => {
// the issue here is that the parent widget creates in the
// mounted hook a new widget, which means that it modifies
// in place its list of children. But this list of children is currently
// being visited, so the mount action of the parent could cause a mount
// action of the new child widget, even though it is not ready yet.
expect.assertions(1);
const target = document.createElement("div");
document.body.appendChild(target);
class ParentWidget extends Widget {
mounted() {
const child = new ChildWidget(this);
child.mount(this.el!);
}
}
class ChildWidget extends Widget {
mounted() {
expect(this.el).toBeTruthy();
done();
}
}
const widget = new ParentWidget(env);
await widget.mount(target);
});
test("components are unmounted and destroyed if no longer in DOM", async () => {
let steps: string[] = [];
class ChildWidget extends Widget {
constructor(parent) {
super(parent);
steps.push("init");
}
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { ChildWidget };
state = useState({ ok: true });
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(steps).toEqual(["init", "willstart", "mounted"]);
widget.state.ok = false;
await nextTick();
expect(steps).toEqual(["init", "willstart", "mounted", "willunmount"]);
});
test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => {
let childUnmounted = false;
class ChildWidget extends Widget {
static template = xml``;
willUnmount() {
childUnmounted = true;
}
}
class ParentWidget extends Widget {
static template = xml`
");
});
});
describe("composition", () => {
test("a widget with a sub widget", async () => {
const widget = new WidgetA(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
Hello
world
");
expect(children(widget)[0].__owl__.parent).toBe(widget);
});
test("can use components from the global registry", async () => {
QWeb.registerComponent("WidgetB", WidgetB);
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
world
");
delete QWeb.components["WidgetB"];
});
test("can use dynamic components (the class) if given", async () => {
class A extends Component {
static template = xml`child a`;
}
class B extends Component {
static template = xml`child b`;
}
class App extends Component {
static template = xml``;
state = useState({
child: "a"
});
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
const widget = new App(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("child a");
widget.state.child = "b";
await nextTick();
expect(fixture.innerHTML).toBe("child b");
});
test("don't fallback to global registry if widget defined locally", async () => {
QWeb.registerComponent("WidgetB", WidgetB); // should not use this widget
env.qweb.addTemplate("ParentWidget", `
`);
env.qweb.addTemplate("AnotherWidgetB", `Belgium`);
class AnotherWidgetB extends Widget {}
class ParentWidget extends Widget {
static components = { WidgetB: AnotherWidgetB };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
Belgium
");
delete QWeb.components["WidgetB"];
});
test("can define components in template without t-component", async () => {
env.qweb.addTemplates(`
`);
class C extends Widget {}
class P extends Widget {
static components = { C };
}
const parent = new P(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("
1
");
});
test("display a nice error if it cannot find component", async () => {
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplate("Parent", `
`);
class Parent extends Widget {
static components = { SomeWidget: Widget };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(console.error).toBeCalledTimes(1);
expect((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 () => {
class WidgetC extends Widget {
static template = xml`
Hello
`;
static components = { WidgetB };
widget = useRef("mywidgetb");
}
const widget = new WidgetC(env);
await widget.mount(fixture);
expect(widget.widget.comp).toBeInstanceOf(WidgetB);
});
test("t-refs are bound at proper timing", async () => {
expect.assertions(2);
class ParentWidget extends Widget {
static template = xml`
`)
);
});
test("sub components with some state rendered in a loop", async () => {
let n = 1;
env.qweb.addTemplate("ChildWidget", ``);
class ChildWidget extends Widget {
state: any;
constructor(parent) {
super(parent);
this.state = useState({ n });
n++;
}
}
env.qweb.addTemplate(
"parent",
`
`)
);
});
test("list of sub components inside other nodes", async () => {
// this confuses the patching algorithm...
env.qweb.addTemplate("ChildWidget", `child`);
env.qweb.addTemplates(`
"
);
});
test("list of two sub components inside other nodes", async () => {
// this confuses the patching algorithm...
env.qweb.addTemplate("ChildWidget", `child`);
env.qweb.addTemplates(`
asdf`);
class SubWidget extends Widget {}
class Parent extends Widget {
static components = { SubWidget };
state = useState({ blips: [{ a: "a", id: 1 }] });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("
");
});
});
describe("random stuff/miscellaneous", () => {
test("widget after a t-foreach", async () => {
// this test makes sure that the foreach directive does not pollute sub
// context with the inLoop variable, which is then used in the t-component
// directive as a key
env.qweb.addTemplate(
"Test",
`
txt
`
);
class Test extends Widget {
static components = { widget: Widget };
}
const widget = new Test(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
txttxt
");
});
test("t-on with handler bound to dynamic argument on a t-foreach", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
`);
const items = [1, 2, 3, 4];
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { Child };
onEv(n, ev) {
expect(n).toBe(1);
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget(env, { items });
await widget.mount(fixture);
children(widget)[0].trigger("ev", 43);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("updating widget immediately", async () => {
// in this situation, we protect against a bug that occurred: because of the
// interplay between components and vnodes, a sub widget vnode was patched
// twice.
env.qweb.addTemplate("Parent", `
`);
class Child extends Widget {}
class Parent extends Widget {
static components = { child: Child };
state = useState({ flag: false });
}
env.qweb.addTemplate("Child", `abcdef`);
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
`);
class B extends TestWidget {
name = "B";
constructor(parent, props) {
super(parent, props);
steps.push("B:constructor");
}
}
env.qweb.addTemplate("D", `
D
`);
class D extends TestWidget {
name = "D";
constructor(parent, props) {
super(parent, props);
steps.push("D:constructor");
}
}
env.qweb.addTemplate("E", `
E
`);
class E extends TestWidget {
name = "E";
constructor(parent, props) {
super(parent, props);
steps.push("E:constructor");
}
}
env.qweb.addTemplate("F", `
F
`);
class F extends TestWidget {
name = "F";
constructor(parent, props) {
super(parent, props);
steps.push("F:constructor");
}
}
env.qweb.addTemplate(
"C",
`
C
`
);
class C extends TestWidget {
static components = { D, E, F };
name = "C";
state = useState({ flag: true });
constructor(parent, props) {
super(parent, props);
c = this;
steps.push("C:constructor");
}
}
env.qweb.addTemplate("A", `
A
`);
class A extends TestWidget {
static components = { B, C };
name = "A";
}
const a = new A(env);
await a.mount(fixture);
expect(fixture.innerHTML).toBe(`
`;
state = useState({ n: 42 });
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("
42
");
});
});
describe("async rendering", () => {
test("destroying a widget before start is over", async () => {
let def = makeDeferred();
class W extends Widget {
willStart(): Promise {
return def;
}
}
const w = new W(env);
w.mount(fixture);
expect(w.__owl__.isDestroyed).toBe(false);
expect(w.__owl__.isMounted).toBe(false);
w.destroy();
def.resolve();
await nextTick();
expect(w.__owl__.isDestroyed).toBe(true);
expect(w.__owl__.isMounted).toBe(false);
});
test("destroying/recreating a subwidget with different props (if start is not over)", async () => {
let def = makeDeferred();
let n = 0;
env.qweb.addTemplate("W", `
"
);
});
test("components in a node in a t-foreach ", async () => {
class Child extends Widget {}
class App extends Widget {
static components = { Child };
get items() {
return [1, 2];
}
}
env.qweb.addTemplates(`
`);
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"
1
2
"
);
});
test("properly behave when destroyed/unmounted while rendering ", async () => {
let def = Promise.resolve();
env.qweb.addTemplate("Child", `
`);
class SubChild extends Widget {
willPatch() {
throw new Error("Should not happen!");
}
patched() {
throw new Error("Should not happen!");
}
}
class Child extends Widget {
static components = { SubChild };
mounted() {
// from now on, each rendering in child widget will be delayed (see
// __render)
def = makeDeferred();
}
async __render(f) {
const result = await super.__render(f);
await def;
return result;
}
}
env.qweb.addTemplate(
"Parent",
`
");
// this change triggers a rendering of the parent. This rendering is delayed,
// because child is now waiting for def to be resolved
parent.state.val = "Framboise Girardin";
await nextTick();
// with this, we remove child, and childchild, even though it is not finished
// rendering from previous changes
parent.state.flag = false;
await nextTick();
// we now resolve def, so the child rendering is now complete.
(def).resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test("reuse widget if possible, in some async situation", async () => {
env.qweb.addTemplates(`
ab
`);
let destroyCount = 0;
class ChildA extends Widget {
destroy() {
destroyCount++;
super.destroy();
}
}
class ChildB extends Widget {
willStart(): any {
return new Promise(function() {});
}
}
class Parent extends Widget {
static components = { ChildA, ChildB };
state = useState({ valA: 1, valB: 2, flag: false });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(destroyCount).toBe(0);
parent.state.flag = true;
await nextTick();
expect(destroyCount).toBe(0);
parent.state.valB = 3;
await nextTick();
expect(destroyCount).toBe(0);
});
test("delayed component with t-asyncroot directive", async () => {
env.qweb.addTemplates(`
`);
let def;
class Child extends Widget {}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Widget {
static components = { Child, AsyncChild };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.querySelector(".children")!.innerHTML).toBe("00");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("10");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("11");
});
test("fast component with t-asyncroot directive", async () => {
env.qweb.addTemplates(`
`);
let def;
class Child extends Widget {}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Widget {
static components = { Child, AsyncChild };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.querySelector(".children")!.innerHTML).toBe("00");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("10");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("11");
});
test("t-component with t-asyncroot directive: mixed re-renderings", async () => {
env.qweb.addTemplates(`
/
`);
let def;
class Child extends Widget {
state = useState({ val: 0 });
increment() {
this.state.val++;
}
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Widget {
static components = { Child, AsyncChild };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.querySelector(".children")!.innerHTML).toBe("0/00/0");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("0/10/0");
// click on each Child to increment their local counter
const children = parent.el!.querySelectorAll("span");
children[0]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("1/10/0");
children[1]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("1/11/0");
// finalize first re-rendering (coming from the props update)
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("1/11/1");
});
test("rendering component again in next microtick", async () => {
class Child extends Widget {}
class App extends Widget {
static components = { Child };
async onClick() {
(env as any).flag = true;
this.render();
await Promise.resolve();
this.render();
}
}
env.qweb.addTemplates(`
");
});
});
describe("widget and observable state", () => {
test("widget is rerendered when its state is changed", async () => {
env.qweb.addTemplate("TestWidget", `
`);
class TestWidget extends Widget {
state = useState({ drink: "water" });
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
water
");
widget.state.drink = "beer";
// 2 microtask ticks: one for observer, one for rendering
await nextMicroTick();
await nextMicroTick();
expect(fixture.innerHTML).toBe("
beer
");
});
test("subcomponents cannot change observable state received from parent", async () => {
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplate("Parent", `
`);
class Child extends Widget {
constructor(parent, props) {
super(parent, props);
props.obj.coffee = 2;
}
}
class Parent extends Widget {
state = useState({ obj: { coffee: 1 } });
static components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(console.error).toBeCalledTimes(1);
expect((console.error).mock.calls[0][0].message).toMatch(
'Observed state cannot be changed here! (key: "coffee", val: "2")'
);
console.error = consoleError;
});
});
describe("can deduce template from name", () => {
test("can find template if name of component", async () => {
class ABC extends Widget {}
env.qweb.addTemplate("ABC", "Orval");
const abc = new ABC(env);
await abc.mount(fixture);
expect(fixture.innerHTML).toBe("Orval");
});
test("can find template of parent component", async () => {
class ABC extends Widget {}
class DEF extends ABC {}
env.qweb.addTemplate("ABC", "Orval");
const def = new DEF(env);
await def.mount(fixture);
expect(fixture.innerHTML).toBe("Orval");
});
test("can find template of parent component, defined by template key", async () => {
class ABC extends Widget {
static template = "Achel";
}
class DEF extends ABC {}
env.qweb.addTemplate("Achel", "Orval");
const def = new DEF(env);
await def.mount(fixture);
expect(fixture.innerHTML).toBe("Orval");
});
test("templates are found in proper qweb instance", async () => {
const env2 = makeTestEnv();
env.qweb.addTemplate("ABC", "Rochefort 8");
env2.qweb.addTemplate("ABC", "Rochefort 10");
class ABC extends Widget {}
const abc = new ABC(env);
await abc.mount(fixture);
expect(fixture.innerHTML).toBe("Rochefort 8");
abc.destroy();
const abc2 = new ABC(env2);
await abc2.mount(fixture);
expect(fixture.innerHTML).toBe("Rochefort 10");
});
});
describe("t-slot directive", () => {
test("can define and call slots", async () => {
env.qweb.addTemplates(`
`);
class Dialog extends Widget {}
class Parent extends Widget {
static components = { Dialog };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
"
header
footer
"
);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot();
expect(QWeb.slots["1_header"].toString()).toMatchSnapshot();
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context", async () => {
env.qweb.addTemplates(`
`);
class Dialog extends Widget {}
class Parent extends Widget {
static components = { Dialog };
state = useState({ val: 0 });
doSomething() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
'
'
);
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("content is the default slot", async () => {
env.qweb.addTemplates(`
`);
class Dialog extends Widget {}
class Parent extends Widget {
static components = { Dialog };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("
sts rocks
");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("default slot work with text nodes", async () => {
env.qweb.addTemplates(`
`);
class Dialog extends Widget {}
class Parent extends Widget {
static components = { Dialog };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("
sts rocks
");
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a named slot", async () => {
env.qweb.addTemplates(`
`);
class Dialog extends Widget {}
class Parent extends Widget {
static components = { Dialog };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("
stsrocks
");
expect(QWeb.slots["1_content"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a default slot", async () => {
env.qweb.addTemplates(`
`);
class Dialog extends Widget {}
class Parent extends Widget {
static components = { Dialog };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("
");
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test("basic use, on another key in component", async () => {
env.qweb.addTemplates(`
`);
class SomeComponent extends Widget {
some = useState({ text: "" });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("
");
});
});
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(`
Error handled
hey
`);
const handler = jest.fn();
env.qweb.on("error", null, handler);
class ErrorComponent extends Widget {}
class ErrorBoundary extends Widget {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Widget {
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("
");
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(`
Error handled
Some text
`);
class ErrorComponent extends Widget {
constructor(parent) {
super(parent);
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Widget {
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("
Error handled
");
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(`
Error handled
Some text
`);
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 = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Widget {
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("
Error handled
");
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(`
Error handled
Some text
`);
class ErrorComponent extends Widget {
mounted() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Widget {
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("
Error handled
");
});
test("can catch an error in the willPatch call", async () => {
env.qweb.addTemplates(`
Error handled
`);
class ErrorComponent extends Widget {
willPatch() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Widget {
state = useState({ message: "abc" });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("
");
widget.state.flag = false;
await nextTick();
// we make sure here that no call to __render is done
expect(steps).toEqual(["render", "willUnmount"]);
});
test("state changes in willUnmount will be applied on remount", async () => {
class TestWidget extends Widget {
static template = xml`