Files
owl/web/static/tests/core/widget.test.ts
T

655 lines
19 KiB
TypeScript
Raw Normal View History

2019-01-26 20:23:34 +01:00
import { WEnv, Widget } from "../../src/ts/core/widget";
2019-01-29 10:46:01 +01:00
import { makeTestWEnv, makeTestFixture } from "../helpers";
2019-01-29 15:19:02 +01:00
import { normalize } from "../helpers";
2019-01-16 13:19:58 +01:00
2019-01-25 15:28:21 +01:00
//------------------------------------------------------------------------------
// 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
2019-01-23 14:28:41 +01:00
2019-01-25 15:28:21 +01:00
let fixture: HTMLElement;
let env: WEnv;
2019-01-23 11:12:54 +01:00
2019-01-25 15:28:21 +01:00
beforeEach(() => {
2019-01-29 10:46:01 +01:00
fixture = makeTestFixture();
env = makeTestWEnv();
2019-01-25 15:28:21 +01:00
});
afterEach(() => {
fixture.remove();
});
2019-01-16 13:19:58 +01:00
2019-01-25 15:28:21 +01:00
function nextTick(): Promise<void> {
2019-01-16 13:19:58 +01:00
return Promise.resolve();
}
2019-01-28 21:32:21 +01:00
function children(w: Widget<WEnv, {}>): Widget<WEnv, {}>[] {
const childrenMap = w.__widget__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
// Test widgets
2019-01-28 21:32:21 +01:00
class Counter extends Widget<WEnv, {}> {
name = "counter";
template = `<div><t t-esc="state.counter"/><button t-on-click="inc">Inc</button></div>`;
state = {
counter: 0
};
inc() {
this.updateState({ counter: this.state.counter + 1 });
}
}
2019-01-28 21:32:21 +01:00
class WidgetA extends Widget<WEnv, {}> {
name = "a";
template = `<div>Hello<t t-widget="b"/></div>`;
widgets = { b: WidgetB };
}
2019-01-28 21:32:21 +01:00
class WidgetB extends Widget<WEnv, {}> {
template = `<div>world</div>`;
}
2019-01-25 15:28:21 +01:00
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
2019-01-16 13:19:58 +01:00
describe("basic widget properties", () => {
test("has no el after creation", async () => {
2019-01-25 15:28:21 +01:00
const widget = new Widget(env);
2019-01-16 13:19:58 +01:00
expect(widget.el).toBe(null);
});
test("can be mounted", async () => {
2019-01-25 15:28:21 +01:00
const widget = new Widget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
2019-01-16 13:19:58 +01:00
});
test("can be clicked on and updated", async () => {
2019-01-25 15:28:21 +01:00
const counter = new Counter(env);
2019-01-16 13:19:58 +01:00
const target = document.createElement("div");
await counter.mount(target);
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
2019-01-25 15:28:21 +01:00
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
await button.click();
await nextTick();
2019-01-16 13:19:58 +01:00
expect(target.innerHTML).toBe("<div>1<button>Inc</button></div>");
});
2019-01-21 14:38:44 +01:00
test("widget style and classname", async () => {
2019-01-28 21:32:21 +01:00
class StyledWidget extends Widget<WEnv, {}> {
2019-01-21 15:54:18 +01:00
template = `<div style="font-weight:bold;" class="some-class">world</div>`;
2019-01-21 14:38:44 +01:00
}
2019-01-25 15:28:21 +01:00
const widget = new StyledWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
2019-01-21 15:54:18 +01:00
`<div style="font-weight:bold;" class="some-class">world</div>`
);
2019-01-21 14:38:44 +01:00
});
test("updateState before first render does not trigger a render", async () => {
let renderCalls = 0;
2019-01-28 21:32:21 +01:00
class TestW extends Widget<WEnv, {}> {
async willStart() {
this.updateState({});
}
async render() {
renderCalls++;
return super.render();
}
}
2019-01-25 15:28:21 +01:00
const widget = new TestW(env);
await widget.mount(fixture);
expect(renderCalls).toBe(1);
});
2019-01-27 15:12:35 +01:00
test("keeps a reference to env", async () => {
const widget = new Widget(env);
expect(widget.env).toBe(env);
});
2019-01-27 15:12:35 +01:00
test("do not remove previously rendered dom if not necessary", async () => {
const widget = new Widget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div></div>`);
widget.el!.appendChild(document.createElement("span"));
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
widget.render();
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
});
2019-01-16 13:19:58 +01:00
});
2019-01-16 23:03:51 +01:00
describe("lifecycle hooks", () => {
test("willStart hook is called", async () => {
let willstart = false;
2019-01-28 21:32:21 +01:00
class HookWidget extends Widget<WEnv, {}> {
2019-01-16 23:03:51 +01:00
async willStart() {
willstart = true;
}
}
2019-01-25 15:28:21 +01:00
const widget = new HookWidget(env);
await widget.mount(fixture);
2019-01-16 23:03:51 +01:00
expect(willstart).toBe(true);
});
test("mounted hook is not called if not in DOM", async () => {
let mounted = false;
2019-01-28 21:32:21 +01:00
class HookWidget extends Widget<WEnv, {}> {
2019-01-16 23:03:51 +01:00
async mounted() {
mounted = true;
}
}
2019-01-25 15:28:21 +01:00
const widget = new HookWidget(env);
2019-01-16 23:03:51 +01:00
const target = document.createElement("div");
await widget.mount(target);
expect(mounted).toBe(false);
});
2019-01-21 14:54:13 +01:00
test("mounted hook is called if mounted in DOM", async () => {
let mounted = false;
2019-01-28 21:32:21 +01:00
class HookWidget extends Widget<WEnv, {}> {
2019-01-21 14:54:13 +01:00
async mounted() {
mounted = true;
}
}
2019-01-25 15:28:21 +01:00
const widget = new HookWidget(env);
await widget.mount(fixture);
2019-01-21 14:54:13 +01:00
expect(mounted).toBe(true);
});
2019-01-21 16:26:51 +01:00
test("willStart hook is called on subwidget", async () => {
let ok = false;
2019-01-28 21:32:21 +01:00
class ParentWidget extends Widget<WEnv, {}> {
name = "a";
template = `<div><t t-widget="child"/></div>`;
widgets = { child: ChildWidget };
}
2019-01-28 21:32:21 +01:00
class ChildWidget extends Widget<WEnv, {}> {
async willStart() {
ok = true;
}
}
2019-01-25 15:28:21 +01:00
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(ok).toBe(true);
});
2019-01-21 16:26:51 +01:00
test("mounted hook is called on subwidgets, in proper order", async () => {
2019-01-25 11:01:43 +01:00
expect.assertions(4);
2019-01-21 16:26:51 +01:00
let parentMounted = false;
let childMounted = false;
2019-01-28 21:32:21 +01:00
class ParentWidget extends Widget<WEnv, {}> {
2019-01-23 22:52:50 +01:00
name = "a";
template = `<div><t t-widget="child"/></div>`;
2019-01-21 16:26:51 +01:00
widgets = { child: ChildWidget };
2019-01-25 11:01:43 +01:00
mounted() {
2019-01-21 16:26:51 +01:00
expect(childMounted).toBe(false);
parentMounted = true;
}
}
2019-01-28 21:32:21 +01:00
class ChildWidget extends Widget<WEnv, {}> {
2019-01-25 11:01:43 +01:00
mounted() {
expect(document.body.contains(this.el)).toBe(true);
2019-01-21 16:26:51 +01:00
expect(parentMounted).toBe(true);
childMounted = true;
}
}
2019-01-25 15:28:21 +01:00
const widget = new ParentWidget(env);
await widget.mount(fixture);
2019-01-25 11:01:43 +01:00
expect(childMounted).toBe(true);
2019-01-21 16:26:51 +01:00
});
test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => {
expect.assertions(3);
let hookCounter = 0;
2019-01-28 21:32:21 +01:00
class ParentWidget extends Widget<WEnv, {}> {
name = "a";
state = { ok: false };
template = `
2019-01-25 15:28:21 +01:00
<div>
<t t-if="state.ok">
<t t-widget="child"/>
</t>
<t t-else="1">
<div/>
</t>
</div>`; // the t-else part in this template is important. This is
// necessary to have a situation that could confuse the vdom
// patching algorithm
widgets = { child: ChildWidget };
}
2019-01-28 21:32:21 +01:00
class ChildWidget extends Widget<WEnv, {}> {
async willStart() {
hookCounter++;
}
mounted() {
expect(hookCounter).toBe(1);
hookCounter++;
}
}
2019-01-25 15:28:21 +01:00
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(hookCounter).toBe(0); // sub widget not created yet
await widget.updateState({ ok: true });
expect(hookCounter).toBe(2);
});
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);
2019-01-28 21:32:21 +01:00
class ParentWidget extends Widget<WEnv, {}> {
name = "a";
mounted() {
const child = new ChildWidget(this);
child.mount(this.el!);
}
}
2019-01-28 21:32:21 +01:00
class ChildWidget extends Widget<WEnv, {}> {
mounted() {
expect(this.el).toBeTruthy();
done();
}
}
2019-01-25 15:28:21 +01:00
const widget = new ParentWidget(env);
await widget.mount(target);
});
2019-01-27 15:40:10 +01:00
test("widgets are unmounted and destroyed if no longer in DOM", async () => {
let steps: string[] = [];
2019-01-28 21:32:21 +01:00
class ParentWidget extends Widget<WEnv, {}> {
2019-01-27 15:40:10 +01:00
name = "a";
state = { ok: true };
template = `
<div>
<t t-if="state.ok"><t t-widget="child"/></t>
</div>`;
widgets = { child: ChildWidget };
}
2019-01-28 21:32:21 +01:00
class ChildWidget extends Widget<WEnv, {}> {
2019-01-27 15:40:10 +01:00
constructor(parent) {
super(parent);
steps.push("init");
}
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
destroyed() {
steps.push("destroyed");
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(steps).toEqual(["init", "willstart", "mounted"]);
await widget.updateState({ ok: false });
expect(steps).toEqual([
"init",
"willstart",
"mounted",
"willunmount",
"destroyed"
]);
});
test("hooks are called in proper order in widget creation/destruction", async () => {
let steps: string[] = [];
2019-01-28 21:32:21 +01:00
class ParentWidget extends Widget<WEnv, {}> {
name = "a";
template = `
<div><t t-widget="child"/></div>`;
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");
}
destroyed() {
steps.push("p destroyed");
}
}
2019-01-28 21:32:21 +01:00
class ChildWidget extends Widget<WEnv, {}> {
constructor(parent) {
super(parent);
steps.push("c init");
}
async willStart() {
steps.push("c willstart");
}
mounted() {
steps.push("c mounted");
}
willUnmount() {
steps.push("c willunmount");
}
destroyed() {
steps.push("c destroyed");
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
widget.destroy();
expect(steps).toEqual([
"p init",
"p willstart",
"c init",
"c willstart",
"p mounted",
"c mounted",
"c willunmount",
"c destroyed",
"p willunmount",
"p destroyed"
]);
});
2019-01-28 16:16:22 +01:00
test("shouldUpdate hook prevent rerendering", async () => {
let shouldUpdate = false;
2019-01-28 21:32:21 +01:00
class TestWidget extends Widget<WEnv, {}> {
2019-01-28 16:16:22 +01:00
name = "a";
template = `<div><t t-esc="props.val"/></div>`;
shouldUpdate() {
return shouldUpdate;
}
}
const widget = new TestWidget(env, { val: 42 });
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>42</div>");
await widget.updateProps({ val: 123 });
expect(fixture.innerHTML).toBe("<div>42</div>");
shouldUpdate = true;
await widget.updateProps({ val: 666 });
expect(fixture.innerHTML).toBe("<div>666</div>");
});
2019-01-16 23:03:51 +01:00
});
describe("destroy method", () => {
test("destroy remove the widget from the DOM", async () => {
2019-01-25 15:28:21 +01:00
const widget = new Widget(env);
await widget.mount(fixture);
2019-01-16 23:03:51 +01:00
expect(document.contains(widget.el)).toBe(true);
widget.destroy();
expect(document.contains(widget.el)).toBe(false);
2019-01-27 15:40:10 +01:00
expect(widget.__widget__.isMounted).toBe(false);
expect(widget.__widget__.isDestroyed).toBe(true);
2019-01-16 23:03:51 +01:00
});
2019-01-26 18:20:49 +01:00
test("destroying a widget twice only call destroyed once", async () => {
let count = 0;
2019-01-28 21:32:21 +01:00
class TestWidget extends Widget<WEnv, {}> {
2019-01-26 18:20:49 +01:00
destroyed() {
count++;
}
}
const widget = new TestWidget(env);
await widget.mount(fixture);
widget.destroy();
expect(count).toBe(1);
widget.destroy();
expect(count).toBe(1);
});
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.__widget__.isDestroyed).toBe(false);
parent.destroy();
expect(child.__widget__.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.__widget__.parent).toBe(parent);
expect(children(parent).length).toBe(1);
child.destroy();
expect(child.__widget__.parent).toBe(null);
expect(children(parent).length).toBe(0);
});
test("destroying a widget before willStart is done", async () => {
let resolve;
let isRendered = false;
let p: Promise<void> = new Promise(function(r) {
resolve = r;
});
2019-01-28 21:32:21 +01:00
class DelayedWidget extends Widget<WEnv, {}> {
willStart() {
return p;
}
}
expect(fixture.innerHTML).toBe("");
const widget = new DelayedWidget(env);
widget.mount(fixture);
expect(widget.__widget__.isStarted).toBe(false);
expect(widget.__widget__.isMounted).toBe(false);
expect(widget.__widget__.isDestroyed).toBe(false);
widget.destroy();
expect(widget.__widget__.isMounted).toBe(false);
expect(widget.__widget__.isStarted).toBe(false);
expect(widget.__widget__.isDestroyed).toBe(true);
resolve();
// Note: should we abandon await and not have to do this?
// TODO: talk to vsc
await nextTick();
await nextTick();
await nextTick();
expect(widget.__widget__.isStarted).toBe(false);
expect(widget.__widget__.isMounted).toBe(false);
expect(widget.__widget__.isDestroyed).toBe(true);
expect(widget.__widget__.vnode).toBe(null);
expect(fixture.innerHTML).toBe("");
expect(isRendered).toBe(false);
});
2019-01-16 23:03:51 +01:00
});
2019-01-20 14:46:36 +01:00
describe("composition", () => {
2019-01-21 15:46:29 +01:00
test("a widget with a sub widget", async () => {
2019-01-25 15:28:21 +01:00
const widget = new WidgetA(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hello<div>world</div></div>");
expect(children(widget)[0].__widget__.parent).toBe(widget);
2019-01-20 14:46:36 +01:00
});
2019-01-21 15:46:29 +01:00
test("t-refs on widget are widgets", async () => {
2019-01-28 21:32:21 +01:00
class WidgetC extends Widget<WEnv, {}> {
2019-01-21 15:54:18 +01:00
name = "a";
2019-01-21 16:26:51 +01:00
template = `<div>Hello<t t-ref="mywidgetb" t-widget="b"/></div>`;
2019-01-21 15:54:18 +01:00
widgets = { b: WidgetB };
}
2019-01-25 15:28:21 +01:00
const widget = new WidgetC(env);
await widget.mount(fixture);
2019-01-21 15:46:29 +01:00
expect(widget.refs.mywidgetb instanceof WidgetB).toBe(true);
});
test("modifying a sub widget", async () => {
2019-01-28 21:32:21 +01:00
class ParentWidget extends Widget<WEnv, {}> {
template = `<div><t t-widget="Counter"/></div>`;
widgets = { Counter };
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div>0<button>Inc</button></div></div>"
);
const button = fixture.getElementsByTagName("button")[0];
await button.click();
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div>1<button>Inc</button></div></div>"
);
});
test("parent's elm for a children === children's elm, even after rerender", async () => {
const widget = new WidgetA(env);
await widget.mount(fixture);
2019-01-26 18:23:43 +01:00
expect((<any>widget.__widget__.vnode!.children![1]).elm).toBe(
(<any>children(widget)[0].__widget__.vnode).elm
);
await children(widget)[0].render();
2019-01-26 18:23:43 +01:00
expect((<any>widget.__widget__.vnode!.children![1]).elm).toBe(
(<any>children(widget)[0].__widget__.vnode).elm
);
});
test("parent env is propagated to child widgets", 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 () => {
2019-01-28 21:32:21 +01:00
class ParentWidget extends Widget<WEnv, {}> {
template = `<div><t t-widget="Counter"/></div>`;
widgets = { 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(
"<div><div>1<button>Inc</button></div></div>"
);
await widget.render();
expect(fixture.innerHTML).toBe(
"<div><div>1<button>Inc</button></div></div>"
);
});
2019-01-28 15:51:20 +01:00
test("sub widgets are destroyed if no longer in dom, then recreated", async () => {
2019-01-28 21:32:21 +01:00
class ParentWidget extends Widget<WEnv, {}> {
2019-01-28 15:51:20 +01:00
name = "a";
state = { ok: true };
template = `
<div><t t-if="state.ok"><t t-widget="counter"/></t></div>`;
widgets = { counter: 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(
"<div><div>1<button>Inc</button></div></div>"
);
await widget.updateState({ ok: false });
expect(fixture.innerHTML).toBe("<div></div>");
await widget.updateState({ ok: true });
expect(fixture.innerHTML).toBe(
"<div><div>0<button>Inc</button></div></div>"
);
});
2019-01-29 15:19:02 +01:00
test("sub widgets rendered in a loop", async () => {
class ChildWidget extends Widget<WEnv, { n: number }> {
name = "c";
template = `<span><t t-esc="props.n"/></span>`;
}
class Parent extends Widget<WEnv, {}> {
name = "p";
template = `
<div>
<t t-foreach="state.numbers" t-as="number">
<t t-widget="ChildWidget" t-props="{n: number}"/>
</t>
</div>`;
state = {
numbers: [1, 2, 3]
};
widgets = { ChildWidget };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe(
normalize(`
<div>
<span>1</span>
<span>2</span>
<span>3</span>
</div>
`)
);
});
2019-01-20 14:46:36 +01:00
});
describe("props evaluation (with t-props directive)", () => {
test("explicit object prop", async () => {
2019-01-28 21:32:21 +01:00
class Parent extends Widget<WEnv, {}> {
name = "a";
template = `<div><t t-widget="child" t-props="{value: state.val}"/></div>`;
widgets = { child: Child };
state = { val: 42 };
}
2019-01-28 21:32:21 +01:00
class Child extends Widget<WEnv, {}> {
template = `<span><t t-esc="state.someval"/></span>`;
state: { someval: number };
constructor(parent: Parent, props: { value: number }) {
super(parent);
this.state = { someval: props.value };
}
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
});
test("object prop value", async () => {
2019-01-28 21:32:21 +01:00
class Parent extends Widget<WEnv, {}> {
name = "a";
template = `<div><t t-widget="child" t-props="state"/></div>`;
widgets = { child: Child };
state = { val: 42 };
}
2019-01-28 21:32:21 +01:00
class Child extends Widget<WEnv, {}> {
template = `<span><t t-esc="state.someval"/></span>`;
state: { someval: number };
constructor(parent: Parent, props: { val: number }) {
super(parent);
this.state = { someval: props.val };
}
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
});
});