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

368 lines
11 KiB
TypeScript
Raw Normal View History

2019-01-26 20:23:34 +01:00
import { QWeb } from "../../src/ts/core/qweb_vdom";
import { idGenerator } from "../../src/ts/core/utils";
import { WEnv, Widget } from "../../src/ts/core/widget";
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(() => {
fixture = document.createElement("div");
document.body.appendChild(fixture);
env = {
qweb: new QWeb(),
getID: idGenerator()
2019-01-16 13:19:58 +01:00
};
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();
}
function children(w: Widget<WEnv>): Widget<WEnv>[] {
const childrenMap = w.__widget__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
// Test widgets
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 });
}
}
class WidgetA extends Widget<WEnv> {
name = "a";
template = `<div>Hello<t t-widget="b"/></div>`;
widgets = { b: WidgetB };
}
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-25 15:28: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-25 15:28: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-25 15:28: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-25 15:28: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-25 15:28: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-25 15:28:21 +01:00
class ParentWidget extends Widget<WEnv> {
name = "a";
template = `<div><t t-widget="child"/></div>`;
widgets = { child: ChildWidget };
}
2019-01-25 15:28: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-25 15:28: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-25 15:28: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-25 15:28: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-25 15:28: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-25 15:28:21 +01:00
class ParentWidget extends Widget<WEnv> {
name = "a";
mounted() {
const child = new ChildWidget(this);
child.mount(this.el!);
}
}
2019-01-25 15:28: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-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-26 18:20:49 +01:00
test("destroying a widget twice only call destroyed once", async () => {
let count = 0;
class TestWidget extends Widget<WEnv> {
destroyed() {
count++;
}
}
const widget = new TestWidget(env);
await widget.mount(fixture);
widget.destroy();
expect(count).toBe(1);
widget.destroy();
expect(count).toBe(1);
});
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);
});
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-25 15:28: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 () => {
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);
});
2019-01-20 14:46:36 +01:00
});