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("WidgetA", `
Hello
`);
env.qweb.addTemplate("WidgetB", `world
`);
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
function children(w: Component): Component[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
// Test components
class WidgetB extends Component {}
class WidgetA extends Component {
static components = { b: WidgetB };
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("basic widget properties", () => {
test("props is set on root components", async () => {
const widget = new Component(null, {});
expect(widget.props).toEqual({});
});
test("has no el after creation", async () => {
const widget = new Component();
expect(widget.el).toBe(null);
});
test("can be mounted", async () => {
class SomeWidget extends Component {
static template = xml`content
`;
}
const widget = new SomeWidget();
widget.mount(fixture);
await nextTick();
expect(fixture.innerHTML).toBe("content
");
});
test("can be mounted on a documentFragment", async () => {
class SomeWidget extends Component {
static template = xml`content
`;
}
const widget = new SomeWidget();
await widget.mount(document.createDocumentFragment());
expect(fixture.innerHTML).toBe("");
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("content
");
});
test("display a nice message if mounted on a non existing node", async () => {
class SomeWidget extends Component {
static template = xml`content
`;
}
const widget = new SomeWidget();
let error;
try {
await widget.mount(null as any);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(
"Component 'SomeWidget' cannot be mounted: the target is not a valid DOM node.\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)"
);
});
test("display an error message if result of rendering is empty", async () => {
class SomeWidget extends Component {
static template = xml` `;
}
const widget = new SomeWidget();
let error;
try {
await widget.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Rendering 'SomeWidget' did not return anything");
});
test("crashes if it cannot find a template", async () => {
expect.assertions(1);
class SomeWidget extends Component {}
try {
new SomeWidget();
} catch (e) {
expect(e.message).toBe('Could not find template for component "SomeWidget"');
}
});
test("can be clicked on and updated", async () => {
class Counter extends Component {
static template = xml`
Inc
`;
state = useState({
counter: 0
});
}
const counter = new Counter();
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("can handle empty props", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
const parent = new Parent();
await parent.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("
");
});
test("cannot be clicked on and updated if not in DOM", async () => {
class Counter extends Component {
static template = xml`
Inc
`;
state = useState({
counter: 0
});
}
const counter = new Counter();
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(0);
});
test("widget style and classname", async () => {
class StyledWidget extends Component {
static template = xml`
world
`;
}
const widget = new StyledWidget();
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 Component {
static template = xml`
`;
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();
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();
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();
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 Component();
expect(widget.env).toBe(env);
});
test("do not remove previously rendered dom if not necessary", async () => {
class SomeComponent extends Component {
static template = xml`
`;
}
const widget = new SomeComponent();
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();
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();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
console.warn = warn;
});
test("reconciliation alg works for t-foreach in t-foreach, 2", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ rows: [1, 2], cols: ["a", "b"] });
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
""
);
widget.state.rows = [2, 1];
await nextTick();
expect(fixture.innerHTML).toBe(
""
);
});
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();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("t-key on a component with t-if, and a sibling component", async () => {
class Child extends Component {
static template = xml`child `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("child
");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
});
describe("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => {
class App extends Component {
static template = xml`app
`;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe("app
");
});
test("cannot attach a component to an existing node (if not same tagname)", async () => {
class App extends Component {
static template = xml`app `;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
let error;
try {
await app.mount(div, { position: "self" });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)");
});
test("can mount a component (with position='first-child')", async () => {
class App extends Component {
static template = xml`app
`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("app
");
});
test("can mount a component (with position='last-child')", async () => {
class App extends Component {
static template = xml`app
`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("app
");
});
test("default mount option is 'last-child'", async () => {
class App extends Component {
static template = xml`app
`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("app
");
});
});
describe("lifecycle hooks", () => {
test("willStart hook is called", async () => {
let willstart = false;
class HookWidget extends Component {
static template = xml`
`;
async willStart() {
willstart = true;
}
}
const widget = new HookWidget();
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 Component {
static template = xml`
`;
async mounted() {
mounted = true;
}
}
const widget = new HookWidget();
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 Component {
static template = xml`
`;
async mounted() {
mounted = true;
}
}
const widget = new HookWidget();
await widget.mount(fixture);
expect(mounted).toBe(true);
});
test("willStart hook is called on subwidget", async () => {
let ok = false;
class ChildWidget extends Component {
static template = xml`
`;
async willStart() {
ok = true;
}
}
class ParentWidget extends Component {
static template = xml`
`;
static components = { child: ChildWidget };
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
mounted() {
expect(document.body.contains(this.el)).toBe(true);
steps.push("child:mounted");
}
}
class ParentWidget extends Component {
static template = xml`
`;
static components = { ChildWidget };
mounted() {
steps.push("parent:mounted");
}
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
mounted() {
steps.push("childchild:mounted");
}
willUnmount() {
steps.push("childchild:willUnmount");
}
}
class ChildWidget extends Component {
static template = xml`
`;
static components = { childchild: ChildChildWidget };
mounted() {
steps.push("child:mounted");
}
willUnmount() {
steps.push("child:willUnmount");
}
}
class ParentWidget extends Component {
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();
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 Component {
willPatch() {
steps.push("childchild:willPatch");
}
patched() {
steps.push("childchild:patched");
}
}
class ChildWidget extends Component {
static components = { childchild: ChildChildWidget };
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
}
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Component {
static components = { child: ChildWidget };
state = useState({ n: 1 });
willPatch() {
steps.push("parent:willPatch");
}
patched() {
steps.push("parent:patched");
}
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
async willStart() {
steps.push("child:willStart");
}
mounted() {
steps.push("child:mounted");
}
}
class ParentWidget extends Component {
state = useState({ ok: false });
static components = { child: ChildWidget };
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
mounted() {
const child = new ChildWidget(this);
child.mount(this.el!);
}
}
class ChildWidget extends Component {
static template = xml`
`;
mounted() {
expect(this.el).toBeTruthy();
}
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
constructor(parent) {
super(parent);
steps.push("init");
}
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
}
class ParentWidget extends Component {
static template = xml`
`;
static components = { ChildWidget };
state = useState({ ok: true });
}
const widget = new ParentWidget();
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 Component {
static template = xml` `;
willUnmount() {
childUnmounted = true;
}
}
class ParentWidget extends Component {
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();
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 Component {
static template = xml`
`;
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 Component {
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();
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 Component {
static template = xml` `;
willUpdateProps(nextProps) {
expect(nextProps.n).toBe(2);
return def;
}
}
class Parent extends Component {
state = useState({ n: 1 });
static components = { Child: HookWidget };
}
const widget = new Parent();
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 Component {
static template = xml`
`;
state = useState({ a: 1 });
patched() {
n++;
}
}
const widget = new TestWidget();
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 Component {
static template = xml`
`;
patched() {
n++;
}
}
class Parent extends Component {
static template = xml`
`;
state = useState({ a: 1 });
static components = { Child: TestWidget };
}
const widget = new Parent();
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 Component {
static template = xml`
`;
shouldUpdate() {
return shouldUpdate;
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child: TestWidget };
state = useState({ val: 42 });
}
const widget = new Parent();
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 Component {
static template = xml`
`;
constructor(parent, props) {
super(parent, props);
created = true;
}
mounted() {
mounted = true;
}
}
class ParentWidget extends Component {
static template = xml`
`;
static components = { child: ChildWidget };
state = useState({ flag: false });
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
}
class ParentWidget extends Component {
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();
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"
]);
});
});
describe("destroy method", () => {
test("destroy remove the widget from the DOM", async () => {
class SomeComponent extends Component {
static template = xml`
`;
}
const widget = new SomeComponent();
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();
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();
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 Component {
static template = xml`
`;
willStart() {
return def;
}
}
expect(fixture.innerHTML).toBe("");
const widget = new DelayedWidget();
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();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("click
");
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("12 click
");
});
test("destroying a widget before being mounted (2)", async () => {
const steps: string[] = [];
class Child extends Component {
static template = xml` `;
willStart() {
steps.push("willStart");
return makeDeferred();
}
__destroy(parent) {
steps.push("__destroy");
super.__destroy(parent);
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
const prom = parent.mount(fixture);
await nextTick(); // wait for Child to be instantiated
parent.state.flag = false;
await prom;
expect(fixture.innerHTML).toBe("
");
expect(steps).toEqual(["willStart", "__destroy"]);
});
});
describe("composition", () => {
test("a widget with a sub widget", async () => {
const widget = new WidgetA();
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 Component {}
const widget = new ParentWidget();
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();
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();
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 Component {}
class ParentWidget extends Component {
static components = { WidgetB: AnotherWidgetB };
}
const widget = new ParentWidget();
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 Component {}
class P extends Component {
static components = { C };
}
const parent = new P();
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();
class SomeComponent extends Component {}
env.qweb.addTemplate("Parent", `
`);
class Parent extends Component {
static components = { SomeWidget: SomeComponent };
}
const parent = new Parent();
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("modifying a sub widget", async () => {
class Counter extends Component {
static template = xml`
Inc
`;
state = useState({
counter: 0
});
}
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Component {
static components = { Counter };
}
const widget = new ParentWidget();
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 SomeComponent extends Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static components = { Child: SomeComponent };
elem1 = useRef("1");
elem2 = useRef("2");
elem3 = useRef("3");
elem4 = useRef("4");
state = useState({ items: [1, 2, 3] });
}
const parent = new ParentWidget();
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();
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();
await widget.mount(fixture);
expect(children(widget)[0].env).toBe(env);
});
test("rerendering a widget with a sub widget", async () => {
class Counter extends Component {
static template = xml`
Inc
`;
state = useState({
counter: 0
});
}
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Component {
static components = { Counter };
}
const widget = new ParentWidget();
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 () => {
class Counter extends Component {
static template = xml`
Inc
`;
state = useState({
counter: 0
});
}
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Component {
state = useState({ ok: true });
static components = { Counter };
}
const widget = new ParentWidget();
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 rendered in a loop", async () => {
env.qweb.addTemplate("ChildWidget", ` `);
class ChildWidget extends Component {}
env.qweb.addTemplate(
"Parent",
`
`
);
class Parent extends Component {
state = useState({
numbers: [1, 2, 3]
});
static components = { ChildWidget };
}
const parent = new Parent();
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 Component {
state: any;
constructor(parent) {
super(parent);
this.state = useState({ n });
n++;
}
}
env.qweb.addTemplate(
"parent",
`
`
);
class Parent extends Component {
static template = "parent";
state = useState({
numbers: [1, 2, 3]
});
static components = { ChildWidget };
}
const parent = new Parent();
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 Component {
static template = xml`child `;
}
class Parent extends Component {
static template = xml`
hey
noo
test
`;
static components = { ChildWidget };
state = useState({ flag: false });
}
const parent = new Parent();
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 Component {}
class Parent extends Component {
static components = { SubWidget };
state = useState({
blips: [
{ a: "a", id: 1 },
{ b: "b", id: 2 },
{ c: "c", id: 4 }
]
});
}
const parent = new Parent();
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 Component {}
class Parent extends Component {
static components = { SubWidget };
state = useState({ blips: [{ a: "a", id: 1 }] });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("");
});
test("t-component with dynamic value", async () => {
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Component {
static components = { WidgetB };
state = useState({ widget: "WidgetB" });
}
const widget = new ParentWidget();
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 Component {
static components = { WidgetB };
state = useState({ widget: "B" });
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-component not on a node", async () => {
class Child extends Component {
static template = xml`1 `;
}
class Parent extends Component {
static components = { Child };
static template = xml``;
}
const parent = new Parent();
let error;
try {
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(
`Directive 't-component' can only be used on nodes (used on a )`
);
});
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();
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
"
);
});
test("three level of components with collapsing root nodes", async () => {
class GrandChild extends Component {
static template = xml`2
`;
}
class Child extends Component {
static components = { GrandChild };
static template = xml` `;
}
class Parent extends Component {
static components = { Child };
static template = xml` `;
}
const app = new Parent();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("2
");
});
});
describe("props evaluation ", () => {
test("explicit object prop", async () => {
env.qweb.addTemplate("Parent", `
`);
class Child extends Component {
state: { someval: number };
constructor(parent: Parent, props: { value: number }) {
super(parent);
this.state = useState({ someval: props.value });
}
}
class Parent extends Component {
static components = { child: Child };
state = useState({ val: 42 });
}
env.qweb.addTemplate("Child", ` `);
const widget = new Parent();
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 Component {}
env.qweb.addTemplate("Parent", `
`);
class Parent extends Component {
static components = { child: Child };
get greetings() {
const name = "aaron";
return `hello ${name}`;
}
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("hello aaron
");
});
test("t-set works ", async () => {
class Child extends Component {}
env.qweb.addTemplate(
"Parent",
`
`
);
class Parent extends Component {
static components = { child: Child };
}
env.qweb.addTemplate(
"Child",
`
`
);
const widget = new Parent();
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 Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static template = xml`
`;
static components = { Child };
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(``);
});
test("empty class attribute is not added on widget root el", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`
`);
});
test("t-att-class is properly added/removed on widget root el", async () => {
class Child extends Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ a: true, b: false });
}
const widget = new ParentWidget();
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 Component {}
class ParentWidget extends Component {
static components = { Child };
}
env.qweb.addTemplate("Child", `
`);
const widget = new ParentWidget();
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 Component {
state = useState({ d: true });
}
class ParentWidget extends Component {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget();
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 Component {
state = useState({ d: true });
}
class ParentWidget extends Component {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget();
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 Component {
state = useState({ c: true });
mounted() {
this.el!.classList.add("user");
}
}
const widget = new App();
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 SomeComponent extends Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static components = { child: SomeComponent };
}
const widget = new ParentWidget();
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 SomeComponent extends Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static components = { child: SomeComponent };
state = useState({ style: "font-size: 20px" });
}
const widget = new ParentWidget();
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(``);
});
test("error in subcomponent with class", async () => {
class Child extends Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static template = xml`
`;
static components = { Child };
}
const widget = new ParentWidget();
let error;
try {
await widget.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
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 Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static components = { child: Child };
n = 0;
someMethod(ev) {
expect(ev.detail).toBe(43);
this.n++;
}
}
const widget = new ParentWidget();
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 works, even if flush is called many times", async () => {
let flag = false;
let mounted = false;
class Child extends Component {
static template = xml`child `;
mounted() {
mounted = true;
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
doSomething() {
flag = true;
}
}
const parent = new Parent();
parent.mount(fixture);
while (!mounted) {
await nextMicroTick();
Component.scheduler.flush();
}
expect(fixture.innerHTML).toBe("child
");
expect(flag).toBe(false);
fixture.querySelector("span")!.click();
expect(flag).toBe(true);
});
test("t-on with inline statement", async () => {
class Child extends Component {
static template = xml`child `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = { n: 3 };
}
const parent = new Parent();
await parent.mount(fixture);
expect(parent.state.n).toBe(3);
fixture.querySelector("span")!.click();
expect(parent.state.n).toBe(4);
});
test("t-on with handler bound to argument", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
`);
class Child extends Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static components = { child: Child };
onEv(n, ev) {
expect(n).toBe(3);
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static components = { child: Child };
onEv(o, ev) {
expect(o).toEqual({ val: 3 });
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static components = { child: Child };
onEv(o, ev) {
expect(o).toEqual({});
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static components = { child: Child };
onEv(o, ev) {
expect(o).toEqual({});
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
}
class ParentWidget extends Component {
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();
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 Component {
static template = xml`
`;
}
class Child extends Component {
static components = { child: GrandChild };
}
class ParentWidget extends Component {
static components = { child: Child };
onEv1(ev) {
steps.push("onEv1");
}
onEv2(ev) {
steps.push("onEv2");
}
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
}
class Child extends Component {
static components = { child: GrandChild };
}
class ParentWidget extends Component {
static components = { child: Child };
onEv() {}
}
const widget = new ParentWidget();
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 Component {
static template = xml`
`;
}
class Child extends Component {
static components = { GrandChild };
}
class ParentWidget extends Component {
static components = { Child };
onEv() {}
}
const widget = new ParentWidget();
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();
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();
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();
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-on on nested components with collapsing root nodes", async () => {
const steps: string[] = [];
let grandChild;
class GrandChild extends Component {
static template = xml` `;
constructor() {
super(...arguments);
grandChild = this;
}
_onEv() {
steps.push("GrandChild");
}
}
class Child extends Component {
static template = xml` `;
static components = { GrandChild };
_onEv() {
steps.push("Child");
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
_onEv() {
steps.push("Parent");
}
}
const parent = new Parent();
await parent.mount(fixture);
grandChild.trigger("ev");
expect(steps).toEqual(["GrandChild", "Child", "Parent"]);
});
test("t-on on unmounted components", async () => {
const steps: string[] = [];
let child;
class Child extends Component {
static template = xml`
`;
mounted() {
child = this;
}
onClick() {
steps.push("click");
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
await parent.mount(fixture);
let el = child.el;
el.click();
expect(steps).toEqual(["click"]);
parent.unmount();
expect(child.__owl__.isMounted).toBe(false);
el.click();
expect(steps).toEqual(["click"]);
});
test("t-on on destroyed components", async () => {
const steps: string[] = [];
let child;
class Child extends Component {
static template = xml`
`;
mounted() {
child = this;
}
onClick() {
steps.push("click");
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
await parent.mount(fixture);
let el = child.el;
el.click();
expect(steps).toEqual(["click"]);
parent.state.flag = false;
await nextTick();
expect(child.__owl__.isDestroyed).toBe(true);
el.click();
expect(steps).toEqual(["click"]);
});
test("t-on on destroyed components, part 2", async () => {
const steps: string[] = [];
let child;
class GrandChild extends Component {
static template = xml`grandchild
`;
}
class Child extends Component {
static template = xml`
`;
static components = { GrandChild };
gc = useRef("gc");
mounted() {
child = this;
}
onClick() {
steps.push("click");
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true });
}
const parent = new Parent();
await parent.mount(fixture);
let el = child.gc.el;
el.click();
expect(steps).toEqual(["click"]);
parent.state.flag = false;
await nextTick();
expect(child.__owl__.isDestroyed).toBe(true);
el.click();
expect(steps).toEqual(["click"]);
});
test("t-if works with t-component", async () => {
env.qweb.addTemplate("ParentWidget", `
`);
class Child extends Component {}
class ParentWidget extends Component {
static components = { child: Child };
state = useState({ flag: true });
}
env.qweb.addTemplate("Child", "hey ");
const widget = new ParentWidget();
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 Component {}
class ParentWidget extends Component {
static components = { child: Child };
state = useState({ flag: true });
}
env.qweb.addTemplate("Child", "hey ");
const widget = new ParentWidget();
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 Component {}
class ParentWidget extends Component {
static components = { child: Child };
state = useState({ flag: true });
}
env.qweb.addTemplate("Child", "hey ");
const widget = new ParentWidget();
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 Component {}
class ParentWidget extends Component {
static components = { child: Child };
state = useState({ flag: true });
}
env.qweb.addTemplate("Child", "hey ");
const widget = new ParentWidget();
await widget.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe("");
widget.state.flag = false;
await nextTick();
expect(normalize(fixture.innerHTML)).toBe("hey
");
});
test("t-foreach with t-component, and update", async () => {
class Child extends Component {
static template = xml`
`;
state = useState({ val: "A" });
mounted() {
this.state.val = "B";
}
}
class ParentWidget extends Component {
static components = { Child };
static template = xml`
`;
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("A0 A1
");
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("B0 B1
");
});
test("t-set outside modified in t-foreach", async () => {
class SomeWidget extends Component {
static template = xml`
`;
state = useState({values: ['a', 'b']});
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("InLoop: 0
InLoop: 1
EndLoop: 2
");
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set outside modified in t-if", async () => {
class SomeWidget extends Component {
static template = xml`
`;
state = {flag: 'if'};
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
widget.state.flag = 'elif';
await widget.render();
expect(fixture.innerHTML).toBe("");
widget.state.flag = 'false';
await widget.render();
expect(fixture.innerHTML).toBe("");
});
test("t-set in t-if", async () => {
// Weird that code block within 'if' leaks outside of it
// Python does the same
class SomeWidget extends Component {
static template = xml`
`;
state = {flag: 'if'};
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
widget.state.flag = 'elif';
await widget.render();
expect(fixture.innerHTML).toBe("");
widget.state.flag = 'false';
await widget.render();
expect(fixture.innerHTML).toBe("");
});
test("t-set can't alter component", async () => {
class SomeWidget extends Component {
static template = xml`
`;
iter = 1;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(widget.iter).toBe(1);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set can't alter from within callee", async () => {
env.qweb.addTemplate("ChildWidget", `
`);
class SomeWidget extends Component {
static template = xml`
`;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set can't alter in t-call body", async () => {
env.qweb.addTemplate("ChildWidget", `
`);
class SomeWidget extends Component {
static template = xml`
`;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("slot setted value (with t-set) not accessible with t-esc", async () => {
class ChildWidget extends Component {
static template = xml`
`;
}
class SomeWidget extends Component {
static components = { ChildWidget };
static template = xml`
`;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set not altered by child widget", async () => {
let child;
class ChildWidget extends Component {
static template = xml`
`;
iter = 'child';
constructor() {
super(...arguments);
child = this;
}
}
class SomeWidget extends Component {
static components = { ChildWidget };
static template = xml`
`;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(child.iter).toBe('child');
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set outside modified in t-foreach increment-after operator", async () => {
class SomeWidget extends Component {
static template = xml`
`;
state = useState({values: ['a', 'b']});
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("InLoop: 0
InLoop: 1
EndLoop: 0
");
});
test("t-set outside modified in t-foreach increment-before operator", async () => {
class SomeWidget extends Component {
static template = xml`
`;
state = useState({values: ['a', 'b']});
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("InLoop: 0
InLoop: 1
EndLoop: 1
");
});
test("t-on expression in t-foreach", async () => {
class SomeWidget extends Component {
static template = xml`
`;
state = useState({values: ['a', 'b']});
otherState = {vals: []};
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(widget.otherState.vals).toStrictEqual([]);
const buttons = fixture.querySelectorAll('button');
buttons[0].click();
buttons[1].click();
expect(widget.otherState.vals).toStrictEqual(['a', 'b']);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-on expression in t-foreach with t-set", async () => {
class SomeWidget extends Component {
static template = xml`
`;
state = useState({values: ['a', 'b']});
otherState = {vals: []};
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(widget.otherState.vals).toStrictEqual([]);
const buttons = fixture.querySelectorAll('button');
buttons[0].click();
buttons[1].click();
expect(widget.otherState.vals).toStrictEqual(['a_nova_0', 'b_nova_0_1']);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-on method call in t-foreach", async () => {
class SomeWidget extends Component {
static template = xml`
`;
state = useState({values: ['a', 'b']});
otherState = {vals: new Array()};
addVal(val: string) {
this.otherState.vals.push(val);
}
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("0: ameth call
1: bmeth call
");
expect(widget.otherState.vals).toStrictEqual([]);
const buttons = fixture.querySelectorAll('button');
buttons[0].click();
buttons[1].click();
expect(widget.otherState.vals).toStrictEqual(['a', 'b']);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-on expression captured in t-foreach", async () => {
class SomeWidget extends Component {
static template = xml`
`;
arr = ['a', 'b']
otherState = {vals: new Array()};
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
expect(widget.otherState.vals).toStrictEqual([]);
const buttons = fixture.querySelectorAll('button');
buttons[0].click();
buttons[1].click();
expect(widget.otherState.vals).toStrictEqual(['0_0', '1_1']);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
});
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 SomeComponent extends Component {
static template = xml`
`;
}
class Test extends Component {
static components = { widget: SomeComponent };
}
const widget = new Test();
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);
class Child extends Component {
static template = xml`
`;
}
class ParentWidget extends Component {
static template = xml`
`;
static components = { Child };
items = [1, 2, 3, 4];
onEv(n, ev) {
expect(n).toBe(1);
expect(ev.detail).toBe(43);
}
}
const widget = new ParentWidget();
await widget.mount(fixture);
children(widget)[0].trigger("ev", 43);
expect(env.qweb.templates[ParentWidget.template].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 Component {}
class Parent extends Component {
static components = { child: Child };
state = useState({ flag: false });
}
env.qweb.addTemplate("Child", `abcdef `);
const widget = new Parent();
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 Component {}
class Parent extends Component {
static components = { child: Child };
state = useState({ flag: false });
}
env.qweb.addTemplate("Child", `abcdef `);
const widget = new Parent();
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 Component {
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);
}
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();
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",
"E:__patch",
"D:__patch",
"C:__patch",
"B:__patch",
"A:__patch",
"E:mounted",
"D:mounted",
"C:mounted",
"B: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",
"F:__patch",
"D:__patch",
"C:__patch",
"E:willUnmount",
"E:destroy",
"F:mounted",
"D:patched",
"C:patched"
]);
});
test("can inject values in tagged templates", async () => {
const SUBTEMPLATE = xml` `;
class Parent extends Component {
static template = xml`
`;
state = useState({ n: 42 });
}
const widget = new Parent();
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 Component {
static template = xml`
`;
willStart(): Promise {
return def;
}
}
const w = new W();
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 Component {
static template = xml`child: `;
constructor(parent, props) {
super(parent, props);
n++;
}
willStart(): Promise {
return def;
}
}
class W extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ val: 1 });
}
const w = new W();
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();
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();
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