import { Component, WEnv } from "../src/component";
import {
makeDeferred,
makeTestFixture,
makeTestWEnv,
nextMicroTick,
nextTick,
normalize
} from "../demo/static/tests/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: WEnv;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestWEnv();
env.qweb.addTemplate(
"counter",
`
`
);
env.qweb.addTemplate("widgetA", `
Hello
`);
env.qweb.addTemplate("widgetB", `
world
`);
env.qweb.addTemplate("default", "");
});
afterEach(() => {
fixture.remove();
});
class Widget extends Component {}
function children(w: Widget): Widget[] {
const childrenMap = w.__widget__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
// Test widgets
class Counter extends Widget {
// class Counter extends Widget {
template = "counter";
state = {
counter: 0
};
inc() {
this.updateState({ counter: this.state.counter + 1 });
}
}
class WidgetA extends Widget {
template = "widgetA";
widgets = { b: WidgetB };
}
class WidgetB extends Widget {
template = "widgetB";
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("basic widget properties", () => {
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);
const target = document.createElement("div");
await counter.mount(target);
expect(target.innerHTML).toBe("
`);
});
});
describe("lifecycle hooks", () => {
test("willStart hook is called", async () => {
let willstart = false;
class HookWidget extends Widget {
async willStart() {
willstart = true;
}
}
const widget = new HookWidget(env);
await widget.mount(fixture);
expect(willstart).toBe(true);
});
test("mounted hook is not called if not in DOM", async () => {
let mounted = false;
class HookWidget extends Widget {
async mounted() {
mounted = true;
}
}
const widget = new HookWidget(env);
const target = document.createElement("div");
await widget.mount(target);
expect(mounted).toBe(false);
});
test("mounted hook is called if mounted in DOM", async () => {
let mounted = false;
class HookWidget extends Widget {
async mounted() {
mounted = true;
}
}
const widget = new HookWidget(env);
await widget.mount(fixture);
expect(mounted).toBe(true);
});
test("willStart hook is called on subwidget", async () => {
let ok = false;
class ParentWidget extends Widget {
inlineTemplate = `
`;
widgets = { child: ChildWidget };
}
class ChildWidget extends Widget {
async willStart() {
ok = true;
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(ok).toBe(true);
});
test("mounted hook is called on subwidgets, in proper order", async () => {
expect.assertions(4);
let parentMounted = false;
let childMounted = false;
class ParentWidget extends Widget {
inlineTemplate = `
`;
widgets = { child: ChildWidget };
mounted() {
expect(childMounted).toBe(false);
parentMounted = true;
}
}
class ChildWidget extends Widget {
mounted() {
expect(document.body.contains(this.el)).toBe(true);
expect(parentMounted).toBe(true);
childMounted = true;
}
}
const widget = new ParentWidget(env);
await widget.mount(fixture);
expect(childMounted).toBe(true);
});
test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => {
expect.assertions(3);
let hookCounter = 0;
// the t-else part in the template is important. This is
// necessary to have a situation that could confuse the vdom
// patching algorithm
class ParentWidget extends Widget {
inlineTemplate = `
`;
state = { ok: false };
widgets = { child: ChildWidget };
}
class ChildWidget extends Widget {
async willStart() {
hookCounter++;
}
mounted() {
expect(hookCounter).toBe(1);
hookCounter++;
}
}
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);
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[] = [];
class ParentWidget extends Widget {
state = { ok: true };
inlineTemplate = `
`;
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");
}
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[] = [];
class ParentWidget extends Widget {
inlineTemplate = `
"
);
});
test("parent's elm for a children === children's elm, even after rerender", async () => {
const widget = new WidgetA(env);
await widget.mount(fixture);
expect((widget.__widget__.vnode!.children![1]).elm).toBe(
(children(widget)[0].__widget__.vnode).elm
);
await children(widget)[0].render();
expect((widget.__widget__.vnode!.children![1]).elm).toBe(
(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 () => {
class ParentWidget extends Widget {
inlineTemplate = `
"
);
});
test("sub widgets are destroyed if no longer in dom, then recreated", async () => {
class ParentWidget extends Widget {
state = { ok: true };
inlineTemplate = `
"
);
});
test("sub widgets with t-keep-alive are not destroyed if no longer in dom", async () => {
class ParentWidget extends Widget {
state = { ok: true };
inlineTemplate = `
"
);
});
test("sub widgets dom state with t-keep-alive is preserved", async () => {
class ParentWidget extends Widget {
state = { ok: true };
inlineTemplate = `
`)
);
});
test("sub widgets with some state rendered in a loop", async () => {
let n = 1;
class ChildWidget extends Widget {
inlineTemplate = ``;
constructor(parent) {
super(parent);
this.state = { n };
n++;
}
}
env.qweb.addTemplate(
"parent",
`
");
});
});
describe("other directives with t-widget", () => {
test("t-on works as expected", async () => {
let n = 0;
class ParentWidget extends Widget {
inlineTemplate = `
`;
widgets = { child: Child };
someMethod(arg) {
expect(arg).toBe(43);
n++;
}
}
class Child extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
expect(n).toBe(0);
child.trigger("customevent", 43);
expect(n).toBe(1);
child.destroy();
child.trigger("customevent", 43);
expect(n).toBe(1);
});
test("t-if works with t-widget", async () => {
class ParentWidget extends Widget {
inlineTemplate = `
");
});
});
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
class Test extends Widget {
inlineTemplate = `
");
});
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.
class Parent extends Widget {
inlineTemplate = `