diff --git a/web/static/tests/widget.test.ts b/web/static/tests/widget.test.ts index 28964c5d..8b1e81de 100644 --- a/web/static/tests/widget.test.ts +++ b/web/static/tests/widget.test.ts @@ -2,80 +2,92 @@ import { Widget, WEnv } from "../src/ts/core/widget"; import { idGenerator } from "../src/ts/core/utils"; import { QWeb } from "../src/ts/core/qweb_vdom"; -interface Type extends Function { - new (...args: any[]): T; -} +//------------------------------------------------------------------------------ +// Setup and helpers +//------------------------------------------------------------------------------ -type TestEnv = WEnv; -type TestWidget = Widget; +// 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 -function makeWidget(W: Type): TestWidget { - const env: WEnv = { +let fixture: HTMLElement; +let env: WEnv; + +beforeEach(() => { + fixture = document.createElement("div"); + document.body.appendChild(fixture); + env = { qweb: new QWeb(), getID: idGenerator() }; - const w = new W(env); - return w; -} +}); -async function click(el: HTMLElement) { - el.click(); +afterEach(() => { + fixture.remove(); +}); + +function nextTick(): Promise { return Promise.resolve(); } -const template = ` -
-`; - -class Counter extends Widget { - name = "counter"; - template = template; - state = { - counter: 0 - }; - - inc() { - this.updateState({ counter: this.state.counter + 1 }); - } -} +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ describe("basic widget properties", () => { test("has no el after creation", async () => { - const widget = makeWidget(Widget); + const widget = new Widget(env); expect(widget.el).toBe(null); }); test("can be mounted", async () => { - const widget = makeWidget(Widget); - const target = document.createElement("div"); - await widget.mount(target); - expect(target.innerHTML).toBe("
"); + const widget = new Widget(env); + await widget.mount(fixture); + expect(fixture.innerHTML).toBe("
"); }); test("can be clicked on and updated", async () => { - const counter = makeWidget(Counter); + const template = ` +
+ `; + + class Counter extends Widget { + name = "counter"; + template = template; + state = { + counter: 0 + }; + + inc() { + this.updateState({ counter: this.state.counter + 1 }); + } + } + + const counter = new Counter(env); const target = document.createElement("div"); await counter.mount(target); expect(target.innerHTML).toBe("
0
"); - await click((counter.el).getElementsByTagName("button")[0]); + const button = (counter.el).getElementsByTagName("button")[0]; + await button.click(); + await nextTick(); expect(target.innerHTML).toBe("
1
"); }); test("widget style and classname", async () => { - class StyledWidget extends Widget { + class StyledWidget extends Widget { template = `
world
`; } - const widget = makeWidget(StyledWidget); - const target = document.createElement("div"); - await widget.mount(target); - expect(target.innerHTML).toBe( + const widget = new StyledWidget(env); + await widget.mount(fixture); + expect(fixture.innerHTML).toBe( `
world
` ); }); test("updateState before first render does not trigger a render", async () => { let renderCalls = 0; - class TestW extends Widget { + class TestW extends Widget { async willStart() { this.updateState({}); } @@ -84,8 +96,8 @@ describe("basic widget properties", () => { return super.render(); } } - const widget = makeWidget(TestW); - await widget.mount(document.createElement("div")); + const widget = new TestW(env); + await widget.mount(fixture); expect(renderCalls).toBe(1); }); }); @@ -93,25 +105,24 @@ describe("basic widget properties", () => { describe("lifecycle hooks", () => { test("willStart hook is called", async () => { let willstart = false; - class HookWidget extends Widget { + class HookWidget extends Widget { async willStart() { willstart = true; } } - const widget = makeWidget(HookWidget); - const target = document.createElement("div"); - await widget.mount(target); + 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 { + class HookWidget extends Widget { async mounted() { mounted = true; } } - const widget = makeWidget(HookWidget); + const widget = new HookWidget(env); const target = document.createElement("div"); await widget.mount(target); expect(mounted).toBe(false); @@ -119,45 +130,38 @@ describe("lifecycle hooks", () => { test("mounted hook is called if mounted in DOM", async () => { let mounted = false; - class HookWidget extends Widget { + class HookWidget extends Widget { async mounted() { mounted = true; } } - const widget = makeWidget(HookWidget); - const target = document.createElement("div"); - document.body.appendChild(target); - await widget.mount(target); + const widget = new HookWidget(env); + await widget.mount(fixture); expect(mounted).toBe(true); - target.remove(); }); test("willStart hook is called on subwidget", async () => { - expect.assertions(1); let ok = false; - class ParentWidget extends Widget { + class ParentWidget extends Widget { name = "a"; template = `
`; widgets = { child: ChildWidget }; } - class ChildWidget extends Widget { + class ChildWidget extends Widget { async willStart() { ok = true; } } - const widget = makeWidget(ParentWidget); - const target = document.createElement("div"); - document.body.appendChild(target); - await widget.mount(target); + const widget = new ParentWidget(env); + await widget.mount(fixture); expect(ok).toBe(true); - target.remove(); }); test("mounted hook is called on subwidgets, in proper order", async () => { expect.assertions(4); let parentMounted = false; let childMounted = false; - class ParentWidget extends Widget { + class ParentWidget extends Widget { name = "a"; template = `
`; widgets = { child: ChildWidget }; @@ -166,41 +170,38 @@ describe("lifecycle hooks", () => { parentMounted = true; } } - class ChildWidget extends Widget { + class ChildWidget extends Widget { mounted() { expect(document.body.contains(this.el)).toBe(true); expect(parentMounted).toBe(true); childMounted = true; } } - const widget = makeWidget(ParentWidget); - const target = document.createElement("div"); - document.body.appendChild(target); - await widget.mount(target); + const widget = new ParentWidget(env); + await widget.mount(fixture); expect(childMounted).toBe(true); - target.remove(); }); test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => { expect.assertions(3); let hookCounter = 0; - class ParentWidget extends Widget { + class ParentWidget extends Widget { name = "a"; state = { ok: false }; template = ` -
- - - - -
- -
`; // the t-else part in this template is important. This is +
+ + + + +
+ +
`; // 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 }; } - class ChildWidget extends Widget { + class ChildWidget extends Widget { async willStart() { hookCounter++; } @@ -209,14 +210,11 @@ describe("lifecycle hooks", () => { hookCounter++; } } - const widget = makeWidget(ParentWidget); - const target = document.createElement("div"); - document.body.appendChild(target); - await widget.mount(target); + 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); - target.remove(); }); test("mounted hook is correctly called on subwidgets created in mounted hook", async done => { @@ -226,34 +224,30 @@ describe("lifecycle hooks", () => { // 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 { + class ParentWidget extends Widget { name = "a"; mounted() { const child = new ChildWidget(this); child.mount(this.el!); } } - class ChildWidget extends Widget { + class ChildWidget extends Widget { mounted() { expect(this.el).toBeTruthy(); - target.remove(); done(); } } - - const widget = makeWidget(ParentWidget); + const widget = new ParentWidget(env); await widget.mount(target); }); }); describe("destroy method", () => { test("destroy remove the widget from the DOM", async () => { - const widget = makeWidget(Widget); - const target = document.body; - await widget.mount(target); + 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); @@ -261,32 +255,29 @@ describe("destroy method", () => { }); describe("composition", () => { - class WidgetA extends Widget { + class WidgetA extends Widget { name = "a"; template = `
Hello
`; widgets = { b: WidgetB }; } - - class WidgetB extends Widget { + class WidgetB extends Widget { template = `
world
`; } test("a widget with a sub widget", async () => { - const widget = makeWidget(WidgetA); - const target = document.createElement("div"); - await widget.mount(target); - expect(target.innerHTML).toBe("
Hello
world
"); + const widget = new WidgetA(env); + await widget.mount(fixture); + expect(fixture.innerHTML).toBe("
Hello
world
"); }); test("t-refs on widget are widgets", async () => { - class WidgetC extends Widget { + class WidgetC extends Widget { name = "a"; template = `
Hello
`; widgets = { b: WidgetB }; } - const widget = makeWidget(WidgetC); - const target = document.createElement("div"); - await widget.mount(target); + const widget = new WidgetC(env); + await widget.mount(fixture); expect(widget.refs.mywidgetb instanceof WidgetB).toBe(true); }); });