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",
`Inc
`
);
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 () => {
class SomeWidget extends Component {
static template = xml`content
`;
}
const widget = new SomeWidget(env);
widget.mount(fixture);
await nextTick();
expect(fixture.innerHTML).toBe("content
");
});
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);
counter.mount(fixture);
await nextTick();
expect(fixture.innerHTML).toBe("0Inc
");
const button = (counter.el).getElementsByTagName("button")[0];
button.click();
await nextTick();
expect(fixture.innerHTML).toBe("1Inc
");
});
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("0Inc
");
const button = (counter.el).getElementsByTagName("button")[0];
button.click();
await nextTick();
expect(target.innerHTML).toBe("0Inc
");
expect(counter.state.counter).toBe(1);
});
test("widget style and classname", async () => {
class StyledWidget extends Widget {
static template = xml`
world
`;
}
const widget = new StyledWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`world
`);
});
test("changing state before first render does not trigger a render", async () => {
const steps: string[] = [];
class TestW extends Widget {
state = useState({ drinks: 1 });
async willStart() {
this.state.drinks++;
}
async __render(f) {
steps.push("__render");
return super.__render(f);
}
mounted() {
steps.push("mounted");
}
patched() {
steps.push("patched");
}
}
const widget = new TestW(env);
await widget.mount(fixture);
expect(steps).toEqual(["__render", "mounted"]);
});
test("changing state before first render does not trigger a render (with parent)", async () => {
const steps: string[] = [];
class TestW extends Component {
static template = xml`W `;
state = useState({ drinks: 1 });
async willStart() {
this.state.drinks++;
}
async __render(f) {
steps.push("__render");
return super.__render(f);
}
mounted() {
steps.push("mounted");
}
patched() {
steps.push("patched");
}
}
class Parent extends Component {
state = useState({ flag: false });
static components = { TestW };
static template = xml`
`;
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("
");
expect(steps).toEqual([]);
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("W
");
expect(steps).toEqual(["__render", "mounted"]);
});
test("render method wait until rendering is done", async () => {
class TestW extends Component {
static template = xml`
`;
state = { drinks: 1 };
}
const widget = new TestW(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("1
");
widget.state.drinks = 2;
const renderPromise = widget.render();
expect(fixture.innerHTML).toBe("1
");
await renderPromise;
expect(fixture.innerHTML).toBe("2
");
});
test("keeps a reference to env", async () => {
const widget = new Widget(env);
expect(widget.env).toBe(env);
});
test("do not remove previously rendered dom if not necessary", async () => {
const widget = new Widget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`
`);
widget.el!.appendChild(document.createElement("span"));
expect(fixture.innerHTML).toBe(`
`);
widget.render(); // FIXME?
expect(fixture.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`
`;
static components = { Child };
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("child child
");
});
test("reconciliation alg works for t-foreach in t-foreach", async () => {
const warn = console.warn;
console.warn = () => {};
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = { s: [{ blips: ["a1", "a2"] }, { blips: ["b1"] }] };
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
console.warn = warn;
});
test("same t-keys in two different places", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
});
});
describe("lifecycle hooks", () => {
test("willStart hook is called", async () => {
let willstart = false;
class HookWidget extends Widget {
async willStart() {
willstart = true;
}
}
const widget = new HookWidget(env);
await widget.mount(fixture);
expect(willstart).toBe(true);
});
test("mounted hook is not called if not in DOM", async () => {
let mounted = false;
class HookWidget extends Widget {
async mounted() {
mounted = true;
}
}
const widget = new HookWidget(env);
const target = document.createElement("div");
await widget.mount(target);
expect(mounted).toBe(false);
});
test("mounted hook is called if mounted in DOM", async () => {
let mounted = false;
class HookWidget extends Widget {
async mounted() {
mounted = true;
}
}
const widget = new HookWidget(env);
await widget.mount(fixture);
expect(mounted).toBe(true);
});
test("willStart hook is called on subwidget", async () => {
let ok = false;
class ChildWidget extends Widget {
async willStart() {
ok = true;
}
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { child: ChildWidget };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(ok).toBe(true);
});
test("mounted hook is called on subcomponents, in proper order", async () => {
const steps: any[] = [];
class ChildWidget extends Widget {
mounted() {
expect(document.body.contains(this.el)).toBe(true);
steps.push("child:mounted");
}
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { ChildWidget };
mounted() {
steps.push("parent:mounted");
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(steps).toEqual(["child:mounted", "parent:mounted"]);
});
test("mounted hook is called on subsubcomponents, in proper order", async () => {
const steps: any[] = [];
class ChildChildWidget extends Widget {
mounted() {
steps.push("childchild:mounted");
}
willUnmount() {
steps.push("childchild:willUnmount");
}
}
class ChildWidget extends Widget {
static template = xml`
`;
static components = { childchild: ChildChildWidget };
mounted() {
steps.push("child:mounted");
}
willUnmount() {
steps.push("child:willUnmount");
}
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { child: ChildWidget };
state = useState({ flag: false });
mounted() {
steps.push("parent:mounted");
}
willUnmount() {
steps.push("parent:willUnmount");
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(steps).toEqual(["parent:mounted"]);
widget.state.flag = true;
await nextTick();
widget.destroy();
expect(steps).toEqual([
"parent:mounted",
"childchild:mounted",
"child:mounted",
"parent:willUnmount",
"child:willUnmount",
"childchild:willUnmount"
]);
});
test("willPatch, patched hook are called on subsubcomponents, in proper order", async () => {
const steps: any[] = [];
env.qweb.addTemplate("ChildWidget", `
`);
env.qweb.addTemplate("ChildChildWidget", `
`);
class ChildChildWidget extends Widget {
willPatch() {
steps.push("childchild:willPatch");
}
patched() {
steps.push("childchild:patched");
}
}
class ChildWidget extends Widget {
static components = { childchild: ChildChildWidget };
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
}
env.qweb.addTemplate("ParentWidget", `
`);
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 () => {
// 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);
class ParentWidget extends Widget {
mounted() {
const child = new ChildWidget(this);
child.mount(this.el!);
}
}
class ChildWidget extends Widget {
mounted() {
expect(this.el).toBeTruthy();
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture); // wait for ParentWidget
await nextTick(); // wait for ChildWidget
});
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`
`;
static components = { ChildWidget };
state = useState({ n: 0, flag: true });
increment() {
this.state.n += 1;
}
toggleSubWidget() {
this.state.flag = !this.state.flag;
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
widget.increment();
await nextTick();
expect(fixture.innerHTML).toBe("");
widget.toggleSubWidget();
await nextTick();
expect(fixture.innerHTML).toBe("
");
expect(childUnmounted).toBe(true);
});
test("hooks are called in proper order in widget creation/destruction", async () => {
let steps: string[] = [];
class ChildWidget extends Widget {
constructor(parent) {
super(parent);
steps.push("c init");
}
async willStart() {
steps.push("c willstart");
}
mounted() {
steps.push("c mounted");
}
willUnmount() {
steps.push("c willunmount");
}
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { child: ChildWidget };
constructor(parent) {
super(parent);
steps.push("p init");
}
async willStart() {
steps.push("p willstart");
}
mounted() {
steps.push("p mounted");
}
willUnmount() {
steps.push("p willunmount");
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
widget.destroy();
expect(steps).toEqual([
"p init",
"p willstart",
"c init",
"c willstart",
"c mounted",
"p mounted",
"p willunmount",
"c willunmount"
]);
});
test("willUpdateProps hook is called", async () => {
let def = makeDeferred();
env.qweb.addTemplate("Parent", ' ');
class HookWidget extends Widget {
willUpdateProps(nextProps) {
expect(nextProps.n).toBe(2);
return def;
}
}
class Parent extends Widget {
state = useState({ n: 1 });
static components = { Child: HookWidget };
}
env.qweb.addTemplate("HookWidget", ' ');
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("1 ");
widget.state.n = 2;
await nextTick();
expect(fixture.innerHTML).toBe("1 ");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("2 ");
});
test("patched hook is called after updating State", async () => {
let n = 0;
class TestWidget extends Widget {
state = useState({ a: 1 });
patched() {
n++;
}
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(n).toBe(0);
widget.state.a = 1; // empty update, should do nothing
await nextTick();
expect(n).toBe(0);
widget.state.a = 3;
await nextTick();
expect(n).toBe(1);
});
test("patched hook is called after updateProps", async () => {
let n = 0;
class TestWidget extends Widget {
patched() {
n++;
}
}
class Parent extends Widget {
static template = xml`
`;
state = useState({ a: 1 });
static components = { Child: TestWidget };
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(n).toBe(0);
widget.state.a = 2;
await nextTick();
expect(n).toBe(1);
});
test("shouldUpdate hook prevent rerendering", async () => {
let shouldUpdate = false;
class TestWidget extends Widget {
static template = xml`
`;
shouldUpdate() {
return shouldUpdate;
}
}
class Parent extends Widget {
static template = xml`
`;
static components = { Child: TestWidget };
state = useState({ val: 42 });
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
widget.state.val = 123;
await nextTick();
expect(fixture.innerHTML).toBe("");
shouldUpdate = true;
widget.state.val = 666;
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test("sub widget (inside sub node): hooks are correctly called", async () => {
let created = false;
let mounted = false;
class ChildWidget extends Widget {
constructor(parent, props) {
super(parent, props);
created = true;
}
mounted() {
mounted = true;
}
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { child: ChildWidget };
state = useState({ flag: false });
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(created).toBe(false);
expect(mounted).toBe(false);
widget.state.flag = true;
await nextTick();
expect(mounted).toBe(true);
expect(created).toBe(true);
});
test("willPatch/patched hook", async () => {
const steps: string[] = [];
class ChildWidget extends Widget {
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
}
class ParentWidget extends Widget {
static template = xml`
`;
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();
// Not sure about this order. If you disagree, feel free to open an issue...
expect(steps).toEqual([
"parent:willPatch",
"child:willPatch",
"child:patched",
"parent:patched"
]);
});
test("willPatch/patched hook with t-keepalive", async () => {
// we make sure here that willPatch/patched is only called if widget is in
// dom, mounted
const steps: string[] = [];
class ChildWidget extends Widget {
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
willUnmount() {
steps.push("child:willUnmount");
}
mounted() {
steps.push("child:mounted");
}
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { ChildWidget };
state = useState({ n: 1, flag: true });
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(env.qweb.templates[ParentWidget.template].fn.toString()).toMatchSnapshot();
expect(steps).toEqual(["child:mounted"]);
widget.state.flag = false;
await nextTick();
expect(steps).toEqual(["child:mounted", "child:willUnmount"]);
widget.state.flag = true;
await nextTick();
expect(steps).toEqual(["child:mounted", "child:willUnmount", "child:mounted"]);
});
});
describe("destroy method", () => {
test("destroy remove the widget from the DOM", async () => {
const widget = new Widget(env);
await widget.mount(fixture);
expect(document.contains(widget.el)).toBe(true);
widget.destroy();
expect(document.contains(widget.el)).toBe(false);
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
});
test("destroying a parent also destroys its children", async () => {
const parent = new WidgetA(env);
await parent.mount(fixture);
const child = children(parent)[0];
expect(child.__owl__.isDestroyed).toBe(false);
parent.destroy();
expect(child.__owl__.isDestroyed).toBe(true);
});
test("destroy remove the parent/children link", async () => {
const parent = new WidgetA(env);
await parent.mount(fixture);
const child = children(parent)[0];
expect(child.__owl__.parent).toBe(parent);
expect(children(parent).length).toBe(1);
child.destroy();
expect(child.__owl__.parent).toBe(null);
expect(children(parent).length).toBe(0);
});
test("destroying a widget before willStart is done", async () => {
let def = makeDeferred();
let isRendered = false;
class DelayedWidget extends Widget {
willStart() {
return def;
}
}
expect(fixture.innerHTML).toBe("");
const widget = new DelayedWidget(env);
widget.mount(fixture);
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(false);
widget.destroy();
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
def.resolve();
await nextTick();
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
expect(widget.__owl__.vnode).toBe(undefined);
expect(fixture.innerHTML).toBe("");
expect(isRendered).toBe(false);
});
test("destroying a widget before being mounted", async () => {
class GrandChild extends Component {
static template = xml`
`;
}
class Child extends Component {
static template = xml`
click
`;
static components = { GrandChild };
state = useState({ val: 33, flag: false });
doSomething() {
this.state.val = 12;
this.state.flag = true;
this.trigger("some-event");
}
get something() {
return { val: this.state.val };
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ p: 1 });
doStuff() {
this.state.p = 2;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("click
");
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("12 click
");
});
});
describe("composition", () => {
test("a widget with a sub widget", async () => {
const widget = new WidgetA(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
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("");
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("can use dynamic components (the class) if given (with different root tagname)", 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);
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe('Cannot find the definition of component "SomeMispelledWidget"');
expect(console.error).toBeCalledTimes(0);
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`
`;
static components = { Widget };
state = useState({ list: [] });
child = useRef("child");
willPatch() {
expect(this.child.comp).toBeNull();
}
patched() {
expect(this.child.comp).not.toBeNull();
}
}
const parent = new ParentWidget(env);
await parent.mount(fixture);
parent.state.list.push(1);
await nextTick();
});
test("t-refs are bound at proper timing (2)", async () => {
expect.assertions(10);
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class ParentWidget extends Widget {
static components = { Widget };
state = useState({ child1: true, child2: false });
child1 = useRef("child1");
child2 = useRef("child2");
count = 0;
mounted() {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
willPatch() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
if (this.count === 1) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
}
patched() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
if (this.count === 1) {
expect(this.child1.comp).toBeNull();
expect(this.child2.comp).toBeDefined();
}
this.count++;
}
}
const parent = new ParentWidget(env);
await parent.mount(fixture);
parent.state.child2 = true;
await nextTick();
parent.state.child1 = false;
await nextTick();
});
test("modifying a sub widget", async () => {
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Widget {
static components = { Counter };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
const button = fixture.getElementsByTagName("button")[0];
await button.click();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test("refs in a loop", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class ParentWidget extends Widget {
static components = { Child: Widget };
elem1 = useRef("1");
elem2 = useRef("2");
elem3 = useRef("3");
elem4 = useRef("4");
state = useState({ items: [1, 2, 3] });
}
const parent = new ParentWidget(env);
await parent.mount(fixture);
expect(parent.elem1.comp).toBeDefined();
expect(parent.elem2.comp).toBeDefined();
expect(parent.elem3.comp).toBeDefined();
expect(parent.elem4.comp).toBeNull();
});
test("parent's elm for a children === children's elm, even after rerender", async () => {
const widget = new WidgetA(env);
await widget.mount(fixture);
expect((widget.__owl__.vnode!.children![1]).elm).toBe(
(children(widget)[0].__owl__.vnode).elm
);
await children(widget)[0].render();
expect((widget.__owl__.vnode!.children![1]).elm).toBe(
(children(widget)[0].__owl__.vnode).elm
);
});
test("parent env is propagated to child components", async () => {
const widget = new WidgetA(env);
await widget.mount(fixture);
expect(children(widget)[0].env).toBe(env);
});
test("rerendering a widget with a sub widget", async () => {
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Widget {
static components = { Counter };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
const button = fixture.getElementsByTagName("button")[0];
await button.click();
await nextTick();
expect(fixture.innerHTML).toBe("");
await widget.render();
expect(fixture.innerHTML).toBe("");
});
test("sub components are destroyed if no longer in dom, then recreated", async () => {
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Widget {
state = useState({ ok: true });
static components = { Counter };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
const button = fixture.getElementsByTagName("button")[0];
await button.click();
await nextTick();
expect(fixture.innerHTML).toBe("");
widget.state.ok = false;
await nextTick();
expect(fixture.innerHTML).toBe("
");
widget.state.ok = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test("sub components with t-keepalive are not destroyed if no longer in dom", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class ParentWidget extends Widget {
state = useState({ ok: true });
static components = { Counter };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
const button = fixture.getElementsByTagName("button")[0];
await button.click();
await nextTick();
expect(fixture.innerHTML).toBe("");
const counter = children(widget)[0];
expect(counter.__owl__.isMounted).toBe(true);
widget.state.ok = false;
await nextTick();
expect(fixture.innerHTML).toBe("
");
expect(counter.__owl__.isMounted).toBe(false);
widget.state.ok = true;
await nextTick();
expect(counter.__owl__.isMounted).toBe(true);
expect(fixture.innerHTML).toBe("");
});
test("sub components dom state with t-keepalive is preserved", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class InputWidget extends Widget {}
class ParentWidget extends Widget {
state = useState({ ok: true });
static components = { InputWidget };
}
env.qweb.addTemplate("InputWidget", " ");
const widget = new ParentWidget(env);
await widget.mount(fixture);
const input = fixture.getElementsByTagName("input")[0];
input.value = "test";
widget.state.ok = false;
await nextTick();
expect(fixture.innerHTML).toBe("
");
widget.state.ok = true;
await nextTick();
expect(fixture.innerHTML).toBe("
");
const input2 = fixture.getElementsByTagName("input")[0];
expect(input).toBe(input2);
expect(input2.value).toBe("test");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("sub widget with t-ref and t-keepalive", async () => {
class ChildWidget extends Widget {
static template = xml`Hello `;
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { ChildWidget };
state = useState({ ok: true });
child = useRef("child");
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
expect(fixture.innerHTML).toBe("Hello
");
expect(widget.child.comp).toEqual(child);
widget.state.ok = false;
await nextTick();
expect(fixture.innerHTML).toBe("
");
expect(widget.child.comp).toEqual(child);
widget.state.ok = true;
await nextTick();
expect(fixture.innerHTML).toBe("Hello
");
expect(widget.child.comp).toEqual(child);
});
test("sub components rendered in a loop", async () => {
env.qweb.addTemplate("ChildWidget", ` `);
class ChildWidget extends Widget {}
env.qweb.addTemplate(
"Parent",
`
`
);
class Parent extends Widget {
state = useState({
numbers: [1, 2, 3]
});
static components = { ChildWidget };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe(
normalize(`
1
2
3
`)
);
});
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",
`
`
);
class Parent extends Widget {
static template = "parent";
state = useState({
numbers: [1, 2, 3]
});
static components = { ChildWidget };
}
const parent = new Parent(env);
await parent.mount(fixture);
parent.state.numbers = [1, 3];
await nextTick();
expect(normalize(fixture.innerHTML)).toBe(
normalize(`
1
3
`)
);
expect(env.qweb.templates.parent.fn.toString()).toMatchSnapshot();
});
test("sub components between t-ifs", async () => {
// this confuses the patching algorithm...
class ChildWidget extends Widget {
static template = xml`child `;
}
class Parent extends Widget {
static template = xml`
hey
noo
test
`;
static components = { ChildWidget };
state = useState({ flag: false });
}
const parent = new Parent(env);
await parent.mount(fixture);
const child = children(parent)[0];
expect(normalize(fixture.innerHTML)).toBe(
normalize(`
noo
child
`)
);
parent.state.flag = true;
await nextTick();
expect(children(parent)[0]).toBe(child);
expect(child.__owl__.isDestroyed).toBe(false);
expect(normalize(fixture.innerHTML)).toBe(
normalize(`
hey
child
test
`)
);
});
test("list of 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 }, { b: "b", id: 2 }, { c: "c", id: 4 }] });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
""
);
parent.state.blips.splice(0, 1);
await nextTick();
expect(fixture.innerHTML).toBe(
""
);
});
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("");
});
test("t-component with dynamic value", async () => {
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Widget {
static components = { WidgetB };
state = useState({ widget: "WidgetB" });
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-component with dynamic value 2", async () => {
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Widget {
static components = { WidgetB };
state = useState({ widget: "B" });
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("sub components, loops, and shouldUpdate", async () => {
class ChildWidget extends Component {
static template = xml` `;
shouldUpdate(nextProps) {
if (nextProps.val === 12) {
return false;
}
return true;
}
}
class Parent extends Component {
static template = xml`
`;
state = useState({
records: [{ id: 1, val: 1 }, { id: 2, val: 2 }, { id: 3, val: 3 }]
});
static components = { ChildWidget };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe(
"1 2 3
"
);
parent.state.records[0].val = 11;
parent.state.records[1].val = 12;
parent.state.records[2].val = 13;
await nextTick();
expect(normalize(fixture.innerHTML)).toBe(
"11 2 13
"
);
});
});
describe("props evaluation ", () => {
test("explicit object prop", async () => {
env.qweb.addTemplate("Parent", `
`);
class Child extends Widget {
state: { someval: number };
constructor(parent: Parent, props: { value: number }) {
super(parent);
this.state = useState({ someval: props.value });
}
}
class Parent extends Widget {
static components = { child: Child };
state = useState({ val: 42 });
}
env.qweb.addTemplate("Child", ` `);
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("42
");
});
test("accept ES6-like syntax for props (with getters)", async () => {
env.qweb.addTemplate("Child", ` `);
class Child extends Widget {}
env.qweb.addTemplate("Parent", `
`);
class Parent extends Widget {
static components = { child: Child };
get greetings() {
return `hello ${this.props.name}`;
}
}
const widget = new Parent(env, { name: "aaron" });
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("hello aaron
");
});
test("t-set works ", async () => {
class Child extends Widget {}
env.qweb.addTemplate(
"Parent",
`
`
);
class Parent extends Widget {
static components = { child: Child };
}
env.qweb.addTemplate(
"Child",
`
`
);
const widget = new Parent(env);
await widget.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe("42
");
});
});
describe("class and style attributes with t-component", () => {
test("class is properly added on widget root el", async () => {
class Child extends Widget {
static template = xml`
`;
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { Child };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(``);
});
test("t-att-class is properly added/removed on widget root el", async () => {
class Child extends Widget {
static template = xml`
`;
}
class ParentWidget extends Widget {
static template = xml`
`;
static components = { Child };
state = useState({ a: true, b: false });
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(``);
expect(QWeb.TEMPLATES[ParentWidget.template].fn.toString());
widget.state.a = false;
widget.state.b = true;
await nextTick();
expect(fixture.innerHTML).toBe(``);
});
test("class with extra whitespaces", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { Child };
}
env.qweb.addTemplate("Child", `
`);
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(``);
});
test("t-att-class is properly added/removed on widget root el (v2)", async () => {
env.qweb.addTemplates(`
`);
class Child extends Widget {
state = useState({ d: true });
}
class ParentWidget extends Widget {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
const span = fixture.querySelector("span")!;
expect(span.className).toBe("c d a b");
widget.state.b = false;
await nextTick();
expect(span.className).toBe("c d a");
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
widget.state.b = true;
await nextTick();
expect(span.className).toBe("c a b");
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("t-att-class is properly added/removed on widget root el (v3)", async () => {
env.qweb.addTemplates(`
`);
class Child extends Widget {
state = useState({ d: true });
}
class ParentWidget extends Widget {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
const span = fixture.querySelector("span")!;
expect(span.className).toBe("c d a b");
widget.state.b = false;
await nextTick();
expect(span.className).toBe("c d a");
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
widget.state.b = true;
await nextTick();
expect(span.className).toBe("c a b");
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("class on components do not interfere with user defined classes", async () => {
env.qweb.addTemplates(`
`);
class App extends Widget {
state = useState({ c: true });
mounted() {
this.el!.classList.add("user");
}
}
const widget = new App(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe('
');
widget.state.c = false;
await nextTick();
expect(fixture.innerHTML).toBe('
');
});
test("style is properly added on widget root el", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class ParentWidget extends Widget {
static components = { child: Widget };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(``);
});
test("dynamic t-att-style is properly added and updated on widget root el", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class ParentWidget extends Widget {
static components = { child: Widget };
state = useState({ style: "font-size: 20px" });
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe(``);
widget.state.style = "font-size: 30px";
await nextTick();
expect(fixture.innerHTML).toBe(``);
});
});
describe("other directives with t-component", () => {
test("t-on works as expected", async () => {
expect.assertions(4);
env.qweb.addTemplates(`
`);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
n = 0;
someMethod(ev) {
expect(ev.detail).toBe(43);
this.n++;
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
expect(widget.n).toBe(0);
child.trigger("custom-event", 43);
expect(widget.n).toBe(1);
child.destroy();
child.trigger("custom-event", 43);
expect(widget.n).toBe(1);
});
test("t-on with handler bound to argument", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
`);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
onEv(n, ev) {
expect(n).toBe(3);
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
child.trigger("ev", 43);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with handler bound to object", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
`);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
onEv(o, ev) {
expect(o).toEqual({ val: 3 });
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
child.trigger("ev", 43);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with handler bound to empty object", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
`);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
onEv(o, ev) {
expect(o).toEqual({});
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
child.trigger("ev", 43);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with handler bound to empty object (with non empty inner string)", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
`);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
onEv(o, ev) {
expect(o).toEqual({});
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
child.trigger("ev", 43);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with stop and/or prevent modifiers", async () => {
expect.assertions(7);
env.qweb.addTemplates(`
`);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
onEv1(ev) {
expect(ev.defaultPrevented).toBe(false);
expect(ev.cancelBubble).toBe(true);
}
onEv2(ev) {
expect(ev.defaultPrevented).toBe(true);
expect(ev.cancelBubble).toBe(false);
}
onEv3(ev) {
expect(ev.defaultPrevented).toBe(true);
expect(ev.cancelBubble).toBe(true);
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
const child = children(widget)[0];
child.trigger("ev-1");
child.trigger("ev-2");
child.trigger("ev-3");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with self modifier", async () => {
env.qweb.addTemplates(`
`);
const steps: string[] = [];
class GrandChild extends Widget {}
class Child extends Widget {
static components = { child: GrandChild };
}
class ParentWidget extends Widget {
static components = { child: Child };
onEv1(ev) {
steps.push("onEv1");
}
onEv2(ev) {
steps.push("onEv2");
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
const child = children(widget)[0];
const grandChild = children(child)[0];
child.trigger("ev-1");
child.trigger("ev-2");
expect(steps).toEqual(["onEv1", "onEv2"]);
grandChild.trigger("ev-1");
grandChild.trigger("ev-2");
expect(steps).toEqual(["onEv1", "onEv2", "onEv1"]);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with self and prevent modifiers (order matters)", async () => {
env.qweb.addTemplates(`
`);
const steps: boolean[] = [];
class GrandChild extends Widget {}
class Child extends Widget {
static components = { child: GrandChild };
}
class ParentWidget extends Widget {
static components = { child: Child };
onEv() {}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
(fixture).addEventListener("ev", function(e) {
steps.push(e.defaultPrevented);
});
const child = children(widget)[0];
const grandChild = children(child)[0];
child.trigger("ev");
grandChild.trigger("ev");
expect(steps).toEqual([true, false]);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with prevent and self modifiers (order matters)", async () => {
env.qweb.addTemplates(`
`);
const steps: boolean[] = [];
class GrandChild extends Widget {}
class Child extends Widget {
static components = { GrandChild };
}
class ParentWidget extends Widget {
static components = { Child };
onEv() {}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
(fixture).addEventListener("ev", function(e) {
steps.push(e.defaultPrevented);
});
const child = children(widget)[0];
const grandChild = children(child)[0];
child.trigger("ev");
grandChild.trigger("ev");
expect(steps).toEqual([true, true]);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with getter as handler", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ counter: 0 });
get handler() {
this.state.counter++;
return () => {};
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("0
");
let child = children(parent)[0];
child.trigger("ev");
await nextTick();
expect(fixture.innerHTML).toBe("1
");
});
test("t-on with inline statement", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ counter: 0 });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("0
");
let child = children(parent)[0];
child.trigger("ev");
await nextTick();
expect(fixture.innerHTML).toBe("1
");
});
test("t-on with no handler (only modifiers)", async () => {
expect.assertions(2);
class ComponentB extends Component {
static template = xml` `;
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB };
}
class Parent extends Component {
static template = xml`
`;
static components = { ComponentA };
onEv(ev) {
expect(ev.defaultPrevented).toBe(true);
}
}
const parent = new Parent(env);
await parent.mount(fixture);
let componentB = children(children(parent)[0])[0];
componentB.trigger("ev");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("t-if works with t-component", async () => {
env.qweb.addTemplate("ParentWidget", `
`);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
state = useState({ flag: true });
}
env.qweb.addTemplate("Child", "hey ");
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("hey
");
widget.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("
");
widget.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("hey
");
});
test("t-else works with t-component", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
state = useState({ flag: true });
}
env.qweb.addTemplate("Child", "hey ");
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe("");
widget.state.flag = false;
await nextTick();
expect(normalize(fixture.innerHTML)).toBe("hey
");
});
test("t-elif works with t-component", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
state = useState({ flag: true });
}
env.qweb.addTemplate("Child", "hey ");
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe("");
widget.state.flag = false;
await nextTick();
expect(normalize(fixture.innerHTML)).toBe("hey
");
});
test("t-else with empty string works with t-component", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class Child extends Widget {}
class ParentWidget extends Widget {
static components = { child: Child };
state = useState({ flag: true });
}
env.qweb.addTemplate("Child", "hey ");
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe("");
widget.state.flag = false;
await nextTick();
expect(normalize(fixture.innerHTML)).toBe("hey
");
});
});
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("");
});
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("abc
");
widget.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("abcdef
");
});
test("snapshotting compiled code", async () => {
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(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});
test("component semantics", async () => {
let steps: string[] = [];
let c: C;
class TestWidget extends Widget {
name: string = "test";
async willStart() {
steps.push(`${this.name}:willStart`);
}
__render(f) {
steps.push(`${this.name}:render`);
return super.__render(f);
}
__patch(vnode) {
steps.push(`${this.name}:__patch`);
super.__patch(vnode);
}
__mount(vnode, elm) {
steps.push(`${this.name}:__patch(from __mount)`);
return super.__mount(vnode, elm);
}
mounted() {
steps.push(`${this.name}:mounted`);
}
async willUpdateProps() {
steps.push(`${this.name}:willUpdateProps`);
}
willPatch() {
steps.push(`${this.name}:willPatch`);
}
patched() {
steps.push(`${this.name}:patched`);
}
willUnmount() {
steps.push(`${this.name}:willUnmount`);
}
destroy() {
super.destroy();
steps.push(`${this.name}:destroy`);
}
}
env.qweb.addTemplate("B", `B
`);
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(``);
expect(steps).toEqual([
"A:willStart",
"A:render",
"B:constructor",
"B:willStart",
"C:constructor",
"C:willStart",
"B:render",
"C:render",
"D:constructor",
"D:willStart",
"E:constructor",
"E:willStart",
"D:render",
"E:render",
"A:__patch",
"B:__patch(from __mount)",
"C:__patch(from __mount)",
"D:__patch(from __mount)",
"E:__patch(from __mount)",
"B:mounted",
"D:mounted",
"E:mounted",
"C:mounted",
"A:mounted"
]);
// update
steps = [];
c!.state.flag = false;
await nextTick();
expect(steps).toEqual([
"C:render",
"D:willUpdateProps",
"F:constructor",
"F:willStart",
"D:render",
"F:render",
"C:willPatch",
"D:willPatch",
"C:__patch",
"E:willUnmount",
"E:destroy",
"F:__patch(from __mount)",
"F:mounted",
"D:__patch",
"D:patched",
"C:patched"
]);
});
test("can inject values in tagged templates", async () => {
const SUBTEMPLATE = xml` `;
class Parent extends Widget {
static template = xml`
`;
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;
class Child extends Widget {
static template = xml`child: `;
constructor(parent, props) {
super(parent, props);
n++;
}
willStart(): Promise {
return def;
}
}
class W extends Widget {
static template = xml`
`;
static components = { Child };
state = useState({ val: 1 });
}
const w = new W(env);
await w.mount(fixture);
expect(n).toBe(0);
w.state.val = 2;
await nextMicroTick();
expect(n).toBe(1);
w.state.val = 3;
await nextMicroTick();
expect(n).toBe(2);
def.resolve();
await nextTick();
expect(children(w).length).toBe(1);
expect(fixture.innerHTML).toBe("child:3
");
});
test("creating two async components, scenario 1", async () => {
let defA = makeDeferred();
let defB = makeDeferred();
let nbRenderings: number = 0;
class ChildA extends Component {
static template = xml` `;
willStart(): Promise {
return defA;
}
getValue() {
nbRenderings++;
return "a";
}
}
class ChildB extends Component {
static template = xml`b `;
willStart(): Promise {
return defB;
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA, ChildB };
state = useState({ flagA: false, flagB: false });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("
");
parent.state.flagA = true;
await nextTick();
expect(fixture.innerHTML).toBe("
");
parent.state.flagB = true;
await nextTick();
expect(fixture.innerHTML).toBe("
");
defB.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("
");
expect(nbRenderings).toBe(0);
defA.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("a b
");
expect(nbRenderings).toBe(1);
});
test("creating two async components, scenario 2", async () => {
let defA = makeDeferred();
let defB = makeDeferred();
class ChildA extends Component {
static template = xml`a `;
willUpdateProps(): Promise {
return defA;
}
}
class ChildB extends Component {
static template = xml`b `;
willStart(): Promise {
return defB;
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA, ChildB };
state = useState({ valA: 1, valB: 2, flagB: false });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("a1
");
parent.state.valA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("a1
");
parent.state.flagB = true;
await nextTick();
expect(fixture.innerHTML).toBe("a1
");
defB.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("a1
");
defA.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("a2 b2
");
});
test("creating two async components, scenario 3 (patching in the same frame)", async () => {
let defA = makeDeferred();
let defB = makeDeferred();
class ChildA extends Component {
static template = xml`a `;
willUpdateProps(): Promise {
return defA;
}
}
class ChildB extends Component {
static template = xml`b `;
willStart(): Promise {
return defB;
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA, ChildB };
state = useState({ valA: 1, valB: 2, flagB: false });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("a1
");
parent.state.valA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("a1
");
parent.state.flagB = true;
await nextTick();
expect(fixture.innerHTML).toBe("a1
");
defB.resolve();
expect(fixture.innerHTML).toBe("a1
");
defA.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("a2 b2
");
});
test("update a sub-component twice in the same frame", async () => {
const steps: string[] = [];
const defs = [makeDeferred(), makeDeferred()];
let index = 0;
class ChildA extends Component {
static template = xml` `;
willUpdateProps(): Promise {
return defs[index++];
}
patched() {
steps.push("patched");
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA };
state = useState({ valA: 1 });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("1
");
parent.state.valA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("1
");
parent.state.valA = 3;
await nextTick();
expect(fixture.innerHTML).toBe("1
");
defs[0].resolve();
await Promise.resolve();
defs[1].resolve();
await nextTick();
expect(fixture.innerHTML).toBe("3
");
expect(steps).toEqual(["patched"]);
});
test("update a sub-component twice in the same frame, 2", async () => {
const steps: string[] = [];
class ChildA extends Component {
static template = xml` `;
patched() {
steps.push("patched");
}
val() {
steps.push("render");
return this.props.val;
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA };
state = useState({ valA: 1 });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("1
");
parent.state.valA = 2;
await nextMicroTick();
expect(steps).toEqual(["render"]);
await nextMicroTick();
expect(steps).toEqual(["render", "render"]);
expect(fixture.innerHTML).toBe("1
");
parent.state.valA = 3;
await nextMicroTick();
expect(steps).toEqual(["render", "render"]);
await nextMicroTick();
expect(steps).toEqual(["render", "render", "render"]);
expect(fixture.innerHTML).toBe("1
");
await nextTick();
expect(fixture.innerHTML).toBe("3
");
expect(steps).toEqual(["render", "render", "render", "patched"]);
});
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(
""
);
});
test("properly behave when destroyed/unmounted while rendering ", async () => {
const def = makeDeferred();
class SubChild extends Widget {
willPatch() {
throw new Error("Should not happen!");
}
patched() {
throw new Error("Should not happen!");
}
willUpdateProps() {
return def;
}
}
class Child extends Widget {
static template = xml`
`;
static components = { SubChild };
}
class Parent extends Widget {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true, val: "Framboise Lindemans" });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("");
// 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();
expect(fixture.innerHTML).toBe("");
// with this, we remove child, and subchild, even though it is not finished
// rendering from previous changes
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("
");
// we now resolve def, so the child rendering is now complete.
(def).resolve();
await nextTick();
expect(fixture.innerHTML).toBe("
");
});
test.skip("reuse widget if possible, in some async situation", async () => {
// this optimization has been temporarily deactivated
env.qweb.addTemplates(`
a
b
`);
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("rendering component again in next microtick", async () => {
class Child extends Widget {
static template = xml`Child
`;
}
class App extends Widget {
static template = xml`
Click
`;
static components = { Child };
async onClick() {
(env as any).flag = true;
this.render();
await Promise.resolve();
this.render();
}
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("Click
");
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test("concurrent renderings scenario 1", async () => {
const def = makeDeferred();
let stateB;
class ComponentC extends Component {
static template = xml` `;
someValue() {
return this.props.fromB;
}
willUpdateProps() {
return def;
}
}
ComponentC.prototype.someValue = jest.fn(ComponentC.prototype.someValue);
class ComponentB extends Component {
static components = { ComponentC };
static template = xml`
`;
state = useState({ fromB: "b" });
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentC.prototype.someValue).toBeCalledTimes(1);
def.resolve();
await nextTick();
expect(ComponentC.prototype.someValue).toBeCalledTimes(2);
expect(fixture.innerHTML).toBe("");
});
test("concurrent renderings scenario 2", async () => {
// this test asserts that a rendering initiated before another one, and that
// ends after it, is re-mapped to that second rendering
const defs = [makeDeferred(), makeDeferred()];
let index = 0;
let stateB;
class ComponentC extends Component {
static template = xml` `;
willUpdateProps() {
return defs[index++];
}
}
class ComponentB extends Component {
static template = xml`
`;
static components = { ComponentC };
state = useState({ fromB: "b" });
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB };
state = useState({ fromA: 1 });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("");
defs[1].resolve(); // resolve rendering initiated in B
await nextTick();
expect(fixture.innerHTML).toBe("");
defs[0].resolve(); // resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test("concurrent renderings scenario 2bis", async () => {
const defs = [makeDeferred(), makeDeferred()];
let index = 0;
let stateB;
class ComponentC extends Component {
static template = xml` `;
willUpdateProps() {
return defs[index++];
}
}
class ComponentB extends Component {
static template = xml`
`;
static components = { ComponentC };
state = useState({ fromB: "b" });
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB };
state = useState({ fromA: 1 });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("");
defs[0].resolve(); // resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe(""); // TODO: is this what we want?? 2b could be ok too
defs[1].resolve(); // resolve rendering initiated in B
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test("concurrent renderings scenario 3", async () => {
const defB = makeDeferred();
const defsD = [makeDeferred(), makeDeferred()];
let index = 0;
let stateC;
class ComponentD extends Component {
static template = xml` `;
someValue() {
return this.props.fromC;
}
willUpdateProps() {
return defsD[index++];
}
}
ComponentD.prototype.someValue = jest.fn(ComponentD.prototype.someValue);
class ComponentC extends Component {
static template = xml` `;
static components = { ComponentD };
state = useState({ fromC: "c" });
constructor(parent, props) {
super(parent, props);
stateC = this.state;
}
}
class ComponentB extends Component {
static template = xml`
`;
static components = { ComponentC };
willUpdateProps() {
return defB;
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
stateC.fromC = "d";
await nextTick();
expect(fixture.innerHTML).toBe("");
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
expect(fixture.innerHTML).toBe("");
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(ComponentD.prototype.someValue).toBeCalledTimes(1);
expect(fixture.innerHTML).toBe("");
defsD[1].resolve(); // completely resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 4", async () => {
const defB = makeDeferred();
const defsD = [makeDeferred(), makeDeferred()];
let index = 0;
let stateC;
class ComponentD extends Component {
static template = xml` `;
someValue() {
return this.props.fromC;
}
willUpdateProps() {
return defsD[index++];
}
}
ComponentD.prototype.someValue = jest.fn(ComponentD.prototype.someValue);
class ComponentC extends Component {
static template = xml` `;
static components = { ComponentD };
state = useState({ fromC: "c" });
constructor(parent, props) {
super(parent, props);
stateC = this.state;
}
}
class ComponentB extends Component {
static template = xml`
`;
static components = { ComponentC };
willUpdateProps() {
return defB;
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
stateC.fromC = "d";
await nextTick();
expect(fixture.innerHTML).toBe("");
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
expect(fixture.innerHTML).toBe("");
defsD[1].resolve(); // completely resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 5", async () => {
const defsB = [makeDeferred(), makeDeferred()];
let index = 0;
class ComponentB extends Component {
static template = xml`
`;
someValue() {
return this.props.fromA;
}
willUpdateProps() {
return defsB[index++];
}
}
ComponentB.prototype.someValue = jest.fn(ComponentB.prototype.someValue);
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
component.state.fromA = 3;
await nextTick();
expect(fixture.innerHTML).toBe("");
defsB[0].resolve(); // resolve first re-rendering (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(1);
defsB[1].resolve(); // resolve second re-rendering
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 6", async () => {
const defsB = [makeDeferred(), makeDeferred()];
let index = 0;
class ComponentB extends Component {
static template = xml`
`;
someValue() {
return this.props.fromA;
}
willUpdateProps() {
return defsB[index++];
}
}
ComponentB.prototype.someValue = jest.fn(ComponentB.prototype.someValue);
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
component.state.fromA = 3;
await nextTick();
expect(fixture.innerHTML).toBe("");
defsB[1].resolve(); // resolve second re-rendering
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
defsB[0].resolve(); // resolve first re-rendering (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 7", async () => {
class ComponentB extends Component {
static template = xml`
`;
state = useState({ fromB: "b" });
someValue() {
return this.state.fromB;
}
async willUpdateProps() {
this.state.fromB = "c";
}
}
ComponentB.prototype.someValue = jest.fn(ComponentB.prototype.someValue);
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(1);
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 8", async () => {
const def = makeDeferred();
let stateB;
class ComponentB extends Component {
static template = xml`
`;
state = useState({ fromB: "b" });
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
async willUpdateProps(nextProps) {
return def;
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test("concurrent renderings scenario 9", async () => {
// Here is the global idea of this scenario:
// A
// / \
// B C
// |
// D
// A state is updated, triggering a whole re-rendering
// B is async, and blocked
// C (and D) are rendered
// C state is updated, producing a re-rendering of C and D
// this last re-rendering of C should be correctly re-mapped to the whole
// re-rendering
const def = makeDeferred();
let stateC;
class ComponentD extends Component {
static template = xml` `;
}
class ComponentC extends Component {
static template = xml`
`;
static components = { ComponentD };
state = useState({ fromC: "b1" });
constructor(parent, props) {
super(parent, props);
stateC = this.state;
}
}
class ComponentB extends Component {
static template = xml` `;
willUpdateProps() {
return def;
}
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB, ComponentC };
state = useState({ fromA: "a1" });
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("");
component.state.fromA = "a2";
await nextTick();
expect(fixture.innerHTML).toBe("");
stateC.fromC = "b2";
await nextTick();
expect(fixture.innerHTML).toBe("");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
});
describe("widget and observable state", () => {
test("widget is rerendered when its state is changed", async () => {
class TestWidget extends Widget {
static template = xml`
`;
state = useState({ drink: "water" });
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("water
");
widget.state.drink = "beer";
await nextTick();
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);
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe('Observed state cannot be changed here! (key: "coffee", val: "2")');
expect(console.error).toBeCalledTimes(0);
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(`
header
footer
`);
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.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(`
do something
`);
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(
'0 do something
'
);
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'1 do something
'
);
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 2", async () => {
env.qweb.addTemplates(`
User
`);
class Link extends Widget {}
class App extends Widget {
state = useState({ users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] });
static components = { Link };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
''
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
''
);
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 3", async () => {
env.qweb.addTemplates(`
`);
class Link extends Widget {}
class App extends Widget {
state = useState({ users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] });
static components = { Link };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
''
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
''
);
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots are rendered with proper context, part 4", async () => {
env.qweb.addTemplates(`
`);
class Link extends Widget {}
class App extends Widget {
state = useState({ user: { id: 1, name: "Aaron" } });
static components = { Link };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe('');
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.user.name = "David";
await nextTick();
expect(fixture.innerHTML).toBe('');
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("refs are properly bound in slots", async () => {
class Dialog extends Widget {
static template = xml` `;
}
class Parent extends Widget {
static template = xml`
do something
`;
static components = { Dialog };
state = useState({ val: 0 });
button = useRef("myButton");
doSomething() {
this.state.val++;
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
'0 do something
'
);
parent.button.el!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'1 do something
'
);
expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
});
test("content is the default slot", async () => {
env.qweb.addTemplates(`
sts rocks
`);
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(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("default slot work with text nodes", async () => {
env.qweb.addTemplates(`
sts rocks
`);
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(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a named slot", async () => {
env.qweb.addTemplates(`
sts
rocks
`);
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(QWeb.slots["1_content"].toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a default slot", async () => {
env.qweb.addTemplates(`
sts
rocks
`);
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(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("missing slots are ignored", async () => {
env.qweb.addTemplates(`
some content
`);
class Dialog extends Widget {}
class Parent extends Widget {
static components = { Dialog };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("some content
");
});
test("t-debug on a t-set (defining a slot)", async () => {
const consoleLog = console.log;
console.log = jest.fn();
env.qweb.addTemplates(`
abc
`);
class Dialog extends Widget {}
class Parent extends Widget {
static components = { Dialog };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(console.log).toHaveBeenCalledTimes(0);
console.log = consoleLog;
});
test("slot preserves properly parented relationship", async () => {
env.qweb.addTemplates(`
Grand Child
`);
class Child extends Widget {}
class GrandChild extends Widget {}
class Parent extends Widget {
static components = { Child, GrandChild };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("");
const parentChildren = children(parent);
expect(parentChildren.length).toBe(1);
expect(parentChildren[0]).toBeInstanceOf(Child);
const childrenChildren = children(parentChildren[0]);
expect(childrenChildren.length).toBe(1);
expect(childrenChildren[0]).toBeInstanceOf(GrandChild);
});
test("slot are properly rendered if inner props are changed", async () => {
env.qweb.addTemplates(`
SC:
Inc[ ]
`);
class SomeComponent extends Widget {}
class GenericComponent extends Widget {}
class App extends Widget {
static components = { GenericComponent, SomeComponent };
state = useState({ val: 4 });
inc() {
this.state.val++;
}
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("");
(fixture.querySelector("button")).click();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test("slots and wrapper components", async () => {
class Link extends Component {
static template = xml`
`;
}
class A extends Component {
static template = xml` hey`;
static components = { Link: Link };
}
const a = new A(env);
await a.mount(fixture);
expect(fixture.innerHTML).toBe(`hey `);
});
test("template can just return a slot", async () => {
class Child extends Widget {
static template = xml` `;
}
class SlotComponent extends Widget {
static template = xml` `;
}
class Parent extends Widget {
static template = xml`
`;
static components = { SlotComponent, Child };
state = useState({ value: 3 });
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("3
");
expect(QWeb.TEMPLATES[SlotComponent.template].fn.toString()).toMatchSnapshot();
parent.state.value = 5;
await nextTick();
expect(fixture.innerHTML).toBe("5
");
});
});
describe("t-model directive", () => {
test("basic use, on an input", async () => {
class SomeComponent extends Widget {
static template = xml`
`;
state = useState({ text: "" });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("
");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("test
");
expect(env.qweb.templates[SomeComponent.template].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("
");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.some.text).toBe("test");
expect(fixture.innerHTML).toBe("test
");
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test("on an input, type=checkbox", async () => {
env.qweb.addTemplates(`
yes
no
`);
class SomeComponent extends Widget {
state = useState({ flag: false });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe('no
');
let input = fixture.querySelector("input")!;
input.click();
await nextTick();
expect(fixture.innerHTML).toBe('yes
');
expect(comp.state.flag).toBe(true);
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
input.click();
await nextTick();
expect(comp.state.flag).toBe(false);
});
test("on an textarea", async () => {
env.qweb.addTemplates(`
`);
class SomeComponent extends Widget {
state = useState({ text: "" });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("
");
const textarea = fixture.querySelector("textarea")!;
await editInput(textarea, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("test
");
});
test("on an input type=radio", async () => {
env.qweb.addTemplates(`
Choice:
`);
class SomeComponent extends Widget {
state = useState({ choice: "" });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
'Choice:
'
);
const firstInput = fixture.querySelector("input")!;
firstInput.click();
await nextTick();
expect(comp.state.choice).toBe("One");
expect(fixture.innerHTML).toBe(
'Choice: One
'
);
const secondInput = fixture.querySelectorAll("input")[1];
secondInput.click();
await nextTick();
expect(comp.state.choice).toBe("Two");
expect(fixture.innerHTML).toBe(
'Choice: Two
'
);
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test("on a select", async () => {
env.qweb.addTemplates(`
Please select one
Red
Blue
Choice:
`);
class SomeComponent extends Widget {
state = useState({ color: "" });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
'Please select one Red Blue Choice:
'
);
const select = fixture.querySelector("select")!;
select.value = "red";
select.dispatchEvent(new Event("change"));
await nextTick();
expect(comp.state.color).toBe("red");
expect(fixture.innerHTML).toBe(
'Please select one Red Blue Choice: red
'
);
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test("on a select, initial state", async () => {
class SomeComponent extends Widget {
static template = xml`
Please select one
Red
Blue
`;
state = useState({ color: "red" });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
const select = fixture.querySelector("select")!;
expect(select.value).toBe("red");
});
test("on a sub state key", async () => {
class SomeComponent extends Widget {
static template = xml`
`;
state = useState({ something: { text: "" } });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("
");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.something.text).toBe("test");
expect(fixture.innerHTML).toBe("test
");
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test(".lazy modifier", async () => {
class SomeComponent extends Widget {
static template = xml`
`;
state = useState({ text: "" });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("
");
const input = fixture.querySelector("input")!;
input.value = "test";
input.dispatchEvent(new Event("input"));
await nextTick();
expect(comp.state.text).toBe("");
expect(fixture.innerHTML).toBe("
");
input.dispatchEvent(new Event("change"));
await nextTick();
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("test
");
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test(".trim modifier", async () => {
class SomeComponent extends Widget {
static template = xml`
`;
state = useState({ text: "" });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
const input = fixture.querySelector("input")!;
await editInput(input, " test ");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("test
");
});
test(".number modifier", async () => {
class SomeComponent extends Widget {
static template = xml`
`;
state = useState({ number: 0 });
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("0
");
const input = fixture.querySelector("input")!;
await editInput(input, "13");
expect(comp.state.number).toBe(13);
expect(fixture.innerHTML).toBe("13
");
await editInput(input, "invalid");
expect(comp.state.number).toBe("invalid");
expect(fixture.innerHTML).toBe("invalid
");
});
});
describe("environment and plugins", () => {
// some source of external events
let bus = new EventBus();
// definition of a plugin
const somePlugin = env => {
env.someFlag = true;
bus.on("some-event", null, () => {
env.someFlag = !env.someFlag;
env.qweb.forceUpdate();
});
};
test("plugin works as expected", async () => {
somePlugin(env);
class App extends Widget {
static template = xml`
Red
Blue
`;
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("Red
");
bus.trigger("some-event");
await nextTick();
expect(fixture.innerHTML).toBe("Blue
");
});
});
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();
const handler = jest.fn();
env.qweb.on("error", null, handler);
class ErrorComponent extends Widget {
static template = xml`hey
`;
}
class ErrorBoundary extends Widget {
static template = xml`
Error handled
`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Widget {
static template = xml`
`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("");
app.state.flag = true;
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("no component catching error lead to full app destruction", async () => {
expect.assertions(6);
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Widget {
static template = xml`hey
`;
}
class App extends Widget {
static template = xml`
`;
static components = { ErrorComponent };
state = useState({ flag: false });
async render() {
try {
await super.render();
} catch (e) {
expect(e.message).toBe("Cannot read property 'this' of undefined");
}
}
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("");
app.state.flag = true;
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
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();
class ErrorComponent extends Component {
static template = xml`hey
`;
}
class ErrorBoundary extends Component {
static template = xml`
Error handled
`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`
`;
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
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("");
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();
class ErrorComponent extends Widget {
static template = xml`Some text
`;
async willStart() {
// we wait a little bit to be in a different stack frame
await nextTick();
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
static template = xml`
Error handled
`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Widget {
static template = xml`
`;
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
});
test.skip("can catch an error in the mounted call", async () => {
// we do not catch error in mounted anymore
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("");
});
test.skip("can catch an error in the willPatch call", async () => {
// we do not catch error in willPatch anymore
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Widget {
static template = xml`
`;
willPatch() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
static template = xml`
Error handled
`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Widget {
static template = xml`
`;
state = useState({ message: "abc" });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("");
app.state.message = "def";
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toHaveBeenCalledTimes(1);
console.error = consoleError;
});
test("a rendering error will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component {
static template = xml`
`;
}
const app = new App(env);
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error in a sub component will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class Child extends Component {
static template = xml`
`;
}
class App extends Component {
static template = xml`
`;
static components = { Child };
}
const app = new App(env);
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component {
static template = xml`
`;
flag = false;
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("
");
app.flag = true;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
});
describe("top level sub widgets", () => {
test("basic use", async () => {
env.qweb.addTemplates(`
child
`);
class Child extends Widget {}
class Parent extends Widget {
static components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("child1 ");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});
test("sub widget is interactive", async () => {
env.qweb.addTemplates(`
click child
`);
class Child extends Widget {
state = useState({ val: 1 });
inc() {
this.state.val++;
}
}
class Parent extends Widget {
static components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("click child1 ");
const button = fixture.querySelector("button")!;
button.click();
await nextTick();
expect(fixture.innerHTML).toBe("click child2 ");
});
test("can select a sub widget ", async () => {
class Child extends Widget {
static template = xml`CHILD 1 `;
}
class OtherChild extends Widget {
static template = xml`CHILD 2
`;
}
class Parent extends Widget {
static template = xml`
`;
static components = { Child, OtherChild };
}
(env).flag = true;
let parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("CHILD 1 ");
parent.destroy();
(env).flag = false;
parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("CHILD 2
");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("can select a sub widget, part 2", async () => {
class Child extends Widget {
static template = xml`CHILD 1 `;
}
class OtherChild extends Widget {
static template = xml`CHILD 2
`;
}
class Parent extends Widget {
static template = xml`
`;
state = useState({ flag: true });
static components = { Child, OtherChild };
}
let parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("CHILD 1 ");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("CHILD 2
");
});
test("top level sub widget with a parent", async () => {
class ComponentC extends Component {
static template = xml`Hello `;
}
class ComponentB extends Component {
static template = xml` `;
static components = { ComponentC };
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB };
}
const component = new ComponentA(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("Hello
");
});
});
describe("unmounting and remounting", () => {
test("widget can be unmounted and remounted", async () => {
const steps: string[] = [];
class MyWidget extends Widget {
static template = xml`Hey
`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
patched() {
throw new Error("patched should not be called");
}
}
const w = new MyWidget(env);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("Hey
");
expect(steps).toEqual(["willstart", "mounted"]);
w.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["willstart", "mounted", "willunmount"]);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("Hey
");
expect(steps).toEqual(["willstart", "mounted", "willunmount", "mounted"]);
});
test("widget can be mounted twice without ill effect", async () => {
const steps: string[] = [];
class MyWidget extends Widget {
static template = xml`Hey
`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
}
const w = new MyWidget(env);
await w.mount(fixture);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("Hey
");
expect(steps).toEqual(["willstart", "mounted"]);
});
test("state changes in willUnmount do not trigger rerender", async () => {
const steps: string[] = [];
class Child extends Widget {
static template = xml`
`;
state = useState({ n: 2 });
__render(f) {
steps.push("render");
return super.__render(f);
}
willPatch() {
steps.push("willPatch");
}
patched() {
steps.push("patched");
}
willUnmount() {
steps.push("willUnmount");
this.state.n = 3;
}
}
class Parent extends Widget {
static template = xml`
`;
static components = { Child };
state = useState({ val: 1, flag: true });
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe("12
");
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`
`;
state = useState({ val: 1 });
willUnmount() {
this.state.val = 3;
}
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("1
");
widget.unmount();
expect(fixture.innerHTML).toBe("");
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("1
");
widget.unmount();
await widget.mount(fixture, true);
expect(fixture.innerHTML).toBe("3
");
});
});
describe("dynamic root nodes", () => {
test("template with t-if, part 1", async () => {
class TestWidget extends Widget {
static template = xml`
hey
abc
`;
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("hey ");
});
test("template with t-if, part 2", async () => {
class TestWidget extends Widget {
static template = xml`
hey
abc
`;
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("abc
");
});
test("switching between sub branches dynamically", async () => {
class TestWidget extends Widget {
static template = xml`
hey
abc
`;
state = useState({ flag: true });
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("hey ");
widget.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("abc
");
});
test("switching between sub components dynamically", async () => {
class ChildA extends Widget {
static template = xml`hey `;
}
class ChildB extends Widget {
static template = xml`abc
`;
}
class TestWidget extends Widget {
static template = xml`
`;
static components = { ChildA, ChildB };
state = useState({ flag: true });
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("hey ");
widget.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("abc
");
});
});
describe("dynamic t-props", () => {
test("basic use", async () => {
expect.assertions(4);
class Child extends Widget {
static template = xml`
`;
constructor(parent, props) {
super(parent, props);
expect(props).toEqual({ a: 1, b: 2 });
expect(props).not.toBe(widget.some.obj);
}
}
class Parent extends Widget {
static template = xml`
`;
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("3
");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
});
describe("support svg components", () => {
test("add proper namespace to svg", async () => {
class GComp extends Widget {
static template = xml`
`;
}
class Svg extends Widget {
static template = xml`
`;
static components = { GComp };
}
const widget = new Svg(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
' '
);
});
});
describe("t-raw in components", () => {
test("update properly on state changes", async () => {
class TestW extends Widget {
static template = xml`
`;
state = useState({ value: "content " });
}
const widget = new TestW(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("content
");
widget.state.value = "other content ";
await nextTick();
expect(fixture.innerHTML).toBe("other content
");
});
test("can render list of t-raw ", async () => {
class TestW extends Widget {
static template = xml`
`;
state = useState({ items: ["one ", "two ", "tree "] });
}
const widget = new TestW(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
"<b>one</b>one <b>two</b>two <b>tree</b>tree
"
);
});
});