import { Component, Env } from "../src/component";
import {
makeDeferred,
makeTestFixture,
makeTestEnv,
nextMicroTick,
nextTick,
normalize
} 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 widgets
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
env.qweb.addTemplate(
"Counter",
`
`
);
env.qweb.addTemplate("WidgetA", `
Hello
`);
env.qweb.addTemplate("WidgetB", `
world
`);
});
afterEach(() => {
fixture.remove();
});
class Widget extends Component {}
function children(w: Widget): Widget[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
// Test widgets
class Counter extends Widget {
state = {
counter: 0
};
inc() {
this.state.counter++;
}
}
class WidgetA extends Widget {
widgets = { b: WidgetB };
}
class WidgetB extends Widget {}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("basic widget properties", () => {
test("props and state are properly defined", async () => {
const widget = new Widget(env);
expect(widget.props).toEqual({});
expect(widget.state).toEqual(undefined);
});
test("has no el after creation", async () => {
const widget = new Widget(env);
expect(widget.el).toBe(null);
});
test("can be mounted", async () => {
const widget = new Widget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("");
});
test("can be clicked on and updated", async () => {
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("
");
});
test("cannot be clicked on and updated if not in DOM", async () => {
const counter = new Counter(env);
const target = document.createElement("div");
await counter.mount(target);
expect(target.innerHTML).toBe("
`);
class ChildChildWidget extends Widget {
willPatch() {
steps.push("childchild:willPatch");
}
patched() {
steps.push("childchild: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 ParentWidget extends Widget {
state = { ok: false };
widgets = { child: ChildWidget };
}
class ChildWidget extends Widget {
async willStart() {
steps.push("child:willStart");
}
mounted() {
steps.push("child:mounted");
}
}
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 subwidgets created in mounted hook", async done => {
// the issue here is that the parent widget creates in the
// mounted hook a new widget, which means that it modifies
// in place its list of children. But this list of children is currently
// being visited, so the mount action of the parent could cause a mount
// action of the new child widget, even though it is not ready yet.
expect.assertions(1);
const target = document.createElement("div");
document.body.appendChild(target);
class ParentWidget extends Widget {
mounted() {
const child = new ChildWidget(this);
child.mount(this.el!);
}
}
class ChildWidget extends Widget {
mounted() {
expect(this.el).toBeTruthy();
done();
}
}
const widget = new ParentWidget(env);
await widget.mount(target);
});
test("widgets are unmounted and destroyed if no longer in DOM", async () => {
let steps: string[] = [];
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class ParentWidget extends Widget {
state = { ok: true };
widgets = { child: ChildWidget };
}
class ChildWidget extends Widget {
constructor(parent) {
super(parent);
steps.push("init");
}
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
}
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("widgets are unmounted and destroyed if no longer in DOM, even after updateprops", async () => {
let childUnmounted = false;
env.qweb.addTemplate("ChildWidget", ``);
class ChildWidget extends Widget {
willUnmount() {
childUnmounted = true;
}
increment() {
this.state += 1;
}
}
env.qweb.addTemplate(
"ParentWidget",
`
");
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[] = [];
env.qweb.addTemplate("ParentWidget", `
`);
class ParentWidget extends Widget {
widgets = { 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");
}
}
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");
}
}
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 Parent extends Widget {
state = { n: 1 };
widgets = { Child: HookWidget };
}
env.qweb.addTemplate("HookWidget", '');
class HookWidget extends Widget {
willUpdateProps(nextProps) {
expect(nextProps.n).toBe(2);
return def;
}
}
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 = { 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;
env.qweb.addTemplate(
"Parent",
'
'
);
class Parent extends Widget {
state = { a: 1 };
widgets = { Child: TestWidget };
}
class TestWidget extends Widget {
patched() {
n++;
}
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(n).toBe(0);
widget.state.a = 2;
await nextTick();
expect(n).toBe(1);
});
test("patched hook is called after updateEnv", async () => {
let n = 0;
class TestWidget extends Widget {
state = { a: 1 };
patched() {
n++;
}
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(n).toBe(0);
await widget.updateEnv({ isMobile: true });
expect(n).toBe(1);
});
test("shouldUpdate hook prevent rerendering", async () => {
let shouldUpdate = false;
env.qweb.addTemplate(
"Parent",
`
`
);
class ParentWidget extends Widget {
widgets = { child: ChildWidget };
state = { n: 1 };
willPatch() {
steps.push("parent:willPatch");
return "leffe";
}
patched(snapshot) {
expect(snapshot).toBe("leffe");
steps.push("parent:patched");
}
}
class ChildWidget extends Widget {
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child: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[] = [];
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class ParentWidget extends Widget {
widgets = { child: ChildWidget };
state = { n: 1, flag: true };
}
class ChildWidget extends Widget {
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
willUnmount() {
steps.push("child:willUnmount");
}
mounted() {
steps.push("child:mounted");
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
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);
});
});
describe("composition", () => {
test("a widget with a sub widget", async () => {
const widget = new WidgetA(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
Hello
world
");
expect(children(widget)[0].__owl__.parent).toBe(widget);
});
test("throw a nice error if it cannot find widget", async () => {
expect.assertions(1);
env.qweb.addTemplate(
"Parent",
`
`
);
class Parent extends Widget {
widgets = { SomeWidget: Widget };
}
const parent = new Parent(env);
try {
await parent.mount(fixture);
} catch (e) {
expect(e.message).toBe(
'Cannot find the definition of widget "SomeMispelledWidget"'
);
}
});
test("t-refs on widget are widgets", async () => {
env.qweb.addTemplate(
"WidgetC",
`
Hello
`
);
class WidgetC extends Widget {
widgets = { b: WidgetB };
}
const widget = new WidgetC(env);
await widget.mount(fixture);
expect(widget.refs.mywidgetb instanceof WidgetB).toBe(true);
});
test("t-refs are bound at proper timing", async () => {
expect.assertions(2);
env.qweb.addTemplate(
"ParentWidget",
`
`
);
class Parent extends Widget {
state = {
numbers: [1, 2, 3]
};
widgets = { ChildWidget };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe(
normalize(`
123
`)
);
});
test("sub widgets with some state rendered in a loop", async () => {
let n = 1;
env.qweb.addTemplate("ChildWidget", ``);
class ChildWidget extends Widget {
constructor(parent) {
super(parent);
this.state = { n };
n++;
}
}
env.qweb.addTemplate(
"parent",
`
");
});
});
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-widget
// directive as a key
env.qweb.addTemplate(
"Test",
`
txt
`
);
class Test extends Widget {
widgets = { widget: Widget };
}
const widget = new Test(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
txttxt
");
});
test("updating widget immediately", async () => {
// in this situation, we protect against a bug that occurred: because of the
// interplay between widgets and vnodes, a sub widget vnode was patched
// twice.
env.qweb.addTemplate(
"Parent",
`
`
);
class Parent extends Widget {
widgets = { child: Child };
state = { flag: false };
}
env.qweb.addTemplate(
"Child",
`abcdef`
);
class Child extends Widget {}
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
"
);
});
test("widgets in a node in a t-foreach ", async () => {
class Child extends Widget {}
class App extends Widget {
widgets = { Child };
get items() {
return [1, 2];
}
}
env.qweb.addTemplates(`
`);
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"
1
2
"
);
});
test("properly behave when destroyed/unmounted while rendering ", async () => {
let def = Promise.resolve();
env.qweb.addTemplate("Child", `
`);
class Child extends Widget {
widgets = { SubChild };
mounted() {
// from now on, each rendering in child widget will be delayed (see
// _render)
def = makeDeferred();
}
async _render(f, p) {
const result = await super._render(f, p);
await def;
return result;
}
}
class SubChild extends Widget {
willPatch() {
throw new Error("Should not happen!");
}
patched() {
throw new Error("Should not happen!");
}
}
env.qweb.addTemplate(
"Parent",
`
`
);
class Parent extends Widget {
widgets = { Child };
state = { 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();
// with this, we remove child, and childchild, even though it is not finished
// rendering from previous changes
parent.state.flag = false;
await nextTick();
// we now resolve def, so the child rendering is now complete.
(def).resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
});
describe("updating environment", () => {
test("can update widget env", async () => {
const widget = new Widget(env);
expect(widget.env).toBe(env);
await widget.updateEnv({ somekey: 4 });
expect(widget.env).toBe(env);
expect((widget).env.somekey).toBe(4);
});
test("updating widget env does not render widget (if not mounted)", async () => {
let n = 0;
class TestWidget extends Widget {
_render() {
n++;
return super._render();
}
}
const widget = new TestWidget(env);
expect(n).toBe(0);
await widget.updateEnv({ somekey: 4 });
expect(n).toBe(0);
await widget.mount(fixture);
expect(n).toBe(1);
await widget.updateEnv({ somekey: 5 });
expect(n).toBe(2);
widget.unmount();
expect(n).toBe(2);
await widget.updateEnv({ somekey: 5 });
expect(n).toBe(2);
});
test("updating child env does not modify parent env", async () => {
env.qweb.addTemplate("ParentWidget", `
");
});
});
describe("widget and observable state", () => {
test("widget is rerendered when its state is changed", async () => {
env.qweb.addTemplate("TestWidget", `
`);
class TestWidget extends Widget {
state = { drink: "water" };
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
water
");
widget.state.drink = "beer";
// 2 microtask ticks: one for observer, one for rendering
await nextMicroTick();
await nextMicroTick();
expect(fixture.innerHTML).toBe("
beer
");
});
test("subwidgets cannot change observable state received from parent", async () => {
expect.assertions(1);
env.qweb.addTemplate(
"Parent",
`
`
);
class Parent extends Widget {
state = { obj: { coffee: 1 } };
widgets = { Child };
}
class Child extends Widget {
constructor(parent, props) {
super(parent, props);
props.coffee = 2;
}
}
const parent = new Parent(env);
try {
await parent.mount(fixture);
} catch (e) {
expect(e.message).toBe(
'Observed state cannot be changed here! (key: "coffee", val: "2")'
);
}
});
test("widget can add observed keys to its state", async () => {
env.qweb.addTemplate(
"TestWidget",
`
`
);
class TestWidget extends Widget {
state: any = { a: 1 };
}
const widget = new TestWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("