import { Component, Env } from "../src/component"; import { makeDeferred, makeTestFixture, makeTestWEnv, 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 = 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.__owl__.children; return Object.keys(childrenMap).map(id => childrenMap[id]); } // Test widgets class Counter extends Widget { template = "counter"; state = { counter: 0 }; inc() { this.state.counter++; } } class WidgetA extends Widget { template = "widgetA"; widgets = { b: WidgetB }; } class WidgetB extends Widget { template = "widgetB"; } //------------------------------------------------------------------------------ // 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); const target = document.createElement("div"); await counter.mount(target); expect(target.innerHTML).toBe("
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 { inlineTemplate = `
world
`; } const widget = new StyledWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe( `
world
` ); }); test("changing state before first render does not trigger a render", async () => { let renderCalls = 0; class TestW extends Widget { state = { drinks: 1 }; async willStart() { this.state.drinks++; } async _render() { renderCalls++; return super._render(); } } const widget = new TestW(env); await widget.mount(fixture); expect(renderCalls).toBe(1); }); test("keeps a reference to env", async () => { const widget = new Widget(env); expect(widget.env).toBe(env); }); test("do not remove previously rendered dom if not necessary", async () => { const widget = new Widget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); widget.el!.appendChild(document.createElement("span")); expect(fixture.innerHTML).toBe(`
`); widget.render(); expect(fixture.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 () => { const steps: any[] = []; class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: ChildWidget }; mounted() { steps.push("parent:mounted"); } } class ChildWidget extends Widget { mounted() { expect(document.body.contains(this.el)).toBe(true); steps.push("child:mounted"); } } const widget = new ParentWidget(env); await widget.mount(fixture); expect(steps).toEqual(["child:mounted", "parent:mounted"]); }); test("mounted hook is called on subsubwidgets, in proper order", async () => { const steps: any[] = []; class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: ChildWidget }; state = { flag: false }; mounted() { steps.push("parent:mounted"); } willUnmount() { steps.push("parent:willUnmount"); } } class ChildWidget extends Widget { inlineTemplate = `
`; widgets = { childchild: ChildChildWidget }; mounted() { steps.push("child:mounted"); } willUnmount() { steps.push("child:willUnmount"); } } class ChildChildWidget extends Widget { mounted() { steps.push("childchild:mounted"); } willUnmount() { steps.push("childchild:willUnmount"); } } const widget = new ParentWidget(env); await widget.mount(fixture); expect(steps).toEqual(["parent:mounted"]); widget.state.flag = true; await nextTick(); widget.destroy(); expect(steps).toEqual([ "parent:mounted", "childchild:mounted", "child:mounted", "parent:willUnmount", "child:willUnmount", "childchild:willUnmount" ]); }); test("willPatch, patched hook are called on subsubwidgets, in proper order", async () => { const steps: any[] = []; class ParentWidget extends Widget { inlineTemplate = `
>
`; widgets = { child: ChildWidget }; state = { n: 1 }; willPatch() { steps.push("parent:willPatch"); } patched() { steps.push("parent:patched"); } } class ChildWidget extends Widget { inlineTemplate = `
`; widgets = { childchild: ChildChildWidget }; willPatch() { steps.push("child:willPatch"); } patched() { steps.push("child:patched"); } } class ChildChildWidget extends Widget { inlineTemplate = `
`; 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 class ParentWidget extends Widget { inlineTemplate = `
`; 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[] = []; 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"); } } 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; class ChildWidget extends Widget { inlineTemplate = ``; willUnmount() { childUnmounted = true; } increment() { this.state += 1; } } class ParentWidget extends Widget { widgets = { ChildWidget }; inlineTemplate = `
`; state = { n: 0, flag: true }; increment() { this.state.n += 1; } toggleSubWidget() { this.state.flag = !this.state.flag; } } const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
0
"); widget.increment(); await nextTick(); expect(fixture.innerHTML).toBe("
1
"); 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[] = []; class ParentWidget extends Widget { inlineTemplate = `
`; 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(); class Parent extends Widget { inlineTemplate = ''; state = { n: 1 }; widgets = { Child: HookWidget }; } class HookWidget extends Widget { inlineTemplate = ''; 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; class Parent extends Widget { inlineTemplate = '
'; 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; class Parent extends Widget { inlineTemplate = `
`; state = { val: 42 }; widgets = { Child: TestWidget }; } class TestWidget extends Widget { inlineTemplate = `
`; shouldUpdate() { return shouldUpdate; } } const widget = new Parent(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
42
"); widget.state.val = 123; await nextTick(); expect(fixture.innerHTML).toBe("
42
"); shouldUpdate = true; widget.state.val = 666; await nextTick(); expect(fixture.innerHTML).toBe("
666
"); }); test("sub widget (inside sub node): hooks are correctly called", async () => { let created = false; let mounted = false; class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: ChildWidget }; state = { flag: false }; } class ChildWidget extends Widget { constructor(parent, props) { super(parent, props); created = true; } mounted() { mounted = true; } } const widget = new ParentWidget(env); await widget.mount(fixture); expect(created).toBe(false); expect(mounted).toBe(false); widget.state.flag = true; await nextTick(); expect(mounted).toBe(true); expect(created).toBe(true); }); test("willPatch/patched hook", async () => { const steps: string[] = []; class ParentWidget extends Widget { inlineTemplate = `
`; 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[] = []; class ParentWidget extends Widget { inlineTemplate = `
`; 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__.isStarted).toBe(false); expect(widget.__owl__.isMounted).toBe(false); expect(widget.__owl__.isDestroyed).toBe(false); widget.destroy(); expect(widget.__owl__.isMounted).toBe(false); expect(widget.__owl__.isStarted).toBe(false); expect(widget.__owl__.isDestroyed).toBe(true); def.resolve(); await nextTick(); expect(widget.__owl__.isStarted).toBe(false); 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("t-refs on widget are widgets", async () => { class WidgetC extends Widget { inlineTemplate = `
Hello
`; 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); class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { Widget }; state = { list: [] }; willPatch() { expect(this.refs.child).toBeUndefined(); } patched() { expect(this.refs.child).not.toBeUndefined(); } } const parent = new ParentWidget(env); await parent.mount(fixture); parent.state.list.push(1); await nextTick(); }); test("t-refs are bound at proper timing (2)", async () => { expect.assertions(10); class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { Widget }; state = { child1: true, child2: false }; count = 0; mounted() { expect(this.refs.child1).toBeDefined(); expect(this.refs.child2).toBeUndefined(); } willPatch() { if (this.count === 0) { expect(this.refs.child1).toBeDefined(); expect(this.refs.child2).toBeUndefined(); } if (this.count === 1) { expect(this.refs.child1).toBeDefined(); expect(this.refs.child2).toBeDefined(); } } patched() { if (this.count === 0) { expect(this.refs.child1).toBeDefined(); expect(this.refs.child2).toBeDefined(); } if (this.count === 1) { expect(this.refs.child1).toBeUndefined(); expect(this.refs.child2).toBeDefined(); } this.count++; } } const parent = new ParentWidget(env); await parent.mount(fixture); parent.state.child2 = true; await nextTick(); parent.state.child1 = false; await nextTick(); }); test("modifying a sub widget", async () => { class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { Counter }; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe( "
0
" ); const button = fixture.getElementsByTagName("button")[0]; await button.click(); await nextTick(); expect(fixture.innerHTML).toBe( "
1
" ); }); 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.__owl__.vnode!.children![1]).elm).toBe( (children(widget)[0].__owl__.vnode).elm ); await children(widget)[0].render(); expect((widget.__owl__.vnode!.children![1]).elm).toBe( (children(widget)[0].__owl__.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 = `
`; 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( "
1
" ); await widget.render(); expect(fixture.innerHTML).toBe( "
1
" ); }); test("sub widgets are destroyed if no longer in dom, then recreated", async () => { class ParentWidget extends Widget { state = { ok: true }; inlineTemplate = `
`; 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( "
1
" ); widget.state.ok = false; await nextTick(); expect(fixture.innerHTML).toBe("
"); widget.state.ok = true; await nextTick(); expect(fixture.innerHTML).toBe( "
0
" ); }); test("sub widgets with t-keepalive are not destroyed if no longer in dom", async () => { class ParentWidget extends Widget { state = { ok: true }; inlineTemplate = `
`; 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( "
1
" ); const counter = children(widget)[0]; expect(counter.__owl__.isMounted).toBe(true); widget.state.ok = false; await nextTick(); expect(fixture.innerHTML).toBe("
"); expect(counter.__owl__.isMounted).toBe(false); widget.state.ok = true; await nextTick(); expect(counter.__owl__.isMounted).toBe(true); expect(fixture.innerHTML).toBe( "
1
" ); }); test("sub widgets dom state with t-keepalive is preserved", async () => { class ParentWidget extends Widget { state = { ok: true }; inlineTemplate = `
`; widgets = { InputWidget }; } class InputWidget extends Widget { inlineTemplate = ""; } const widget = new ParentWidget(env); await widget.mount(fixture); const input = fixture.getElementsByTagName("input")[0]; input.value = "test"; widget.state.ok = false; await nextTick(); expect(fixture.innerHTML).toBe("
"); widget.state.ok = true; await nextTick(); expect(fixture.innerHTML).toBe("
"); const input2 = fixture.getElementsByTagName("input")[0]; expect(input).toBe(input2); expect(input2.value).toBe("test"); }); test("sub widgets rendered in a loop", async () => { class ChildWidget extends Widget { inlineTemplate = ``; } class Parent extends Widget { inlineTemplate = `
`; state = { numbers: [1, 2, 3] }; widgets = { ChildWidget }; } const parent = new Parent(env); await parent.mount(fixture); expect(normalize(fixture.innerHTML)).toBe( normalize(`
1 2 3
`) ); }); 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", `
` ); class Parent extends Widget { template = "parent"; state = { numbers: [1, 2, 3] }; widgets = { ChildWidget }; } const parent = new Parent(env); await parent.mount(fixture); parent.state.numbers = [1, 3]; await nextTick(); expect(normalize(fixture.innerHTML)).toBe( normalize(`
1 3
`) ); expect(env.qweb.templates.parent.toString()).toMatchSnapshot(); }); test("sub widgets between t-ifs", async () => { // this confuses the patching algorithm... class ChildWidget extends Widget { inlineTemplate = `child`; } class Parent extends Widget { inlineTemplate = `

hey

noo

test
`; state = { flag: false }; widgets = { ChildWidget }; } const parent = new Parent(env); await parent.mount(fixture); const child = children(parent)[0]; parent.state.flag = true; await nextTick(); expect(children(parent)[0]).toBe(child); expect(child.__owl__.isDestroyed).toBe(false); expect(normalize(fixture.innerHTML)).toBe( normalize(`

hey

child test
`) ); }); }); describe("props evaluation (with t-props directive)", () => { test("explicit object prop", async () => { class Parent extends Widget { inlineTemplate = `
`; widgets = { child: Child }; state = { val: 42 }; } class Child extends Widget { inlineTemplate = ``; 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("
42
"); }); test("object prop value", async () => { class Parent extends Widget { inlineTemplate = `
`; widgets = { child: Child }; state = { val: 42 }; } class Child extends Widget { inlineTemplate = ``; 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("
42
"); }); test("accept ES6-like syntax for props (with getters)", async () => { class Child extends Widget { inlineTemplate = ``; } class Parent extends Widget { inlineTemplate = `
`; widgets = { child: Child }; get greetings() { return `hello ${this.props.name}`; } } const widget = new Parent(env, { name: "aaron" }); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
hello aaron
"); }); test("t-set works with t-props", async () => { class Parent extends Widget { inlineTemplate = `
`; widgets = { child: Child }; } class Child extends Widget { inlineTemplate = ` `; } const widget = new Parent(env); await widget.mount(fixture); expect(normalize(fixture.innerHTML)).toBe("
42
"); }); }); 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 = `
`; widgets = { child: Child }; state = { flag: true }; } class Child extends Widget { inlineTemplate = "hey"; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
hey
"); widget.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
"); widget.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
hey
"); }); test("t-else works with t-widget", async () => { class ParentWidget extends Widget { inlineTemplate = `
somediv
`; widgets = { child: Child }; state = { flag: true }; } class Child extends Widget { inlineTemplate = "hey"; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(normalize(fixture.innerHTML)).toBe("
somediv
"); widget.state.flag = false; await nextTick(); expect(normalize(fixture.innerHTML)).toBe("
hey
"); }); test("t-else with empty string works with t-widget", async () => { class ParentWidget extends Widget { inlineTemplate = `
somediv
`; widgets = { child: Child }; state = { flag: true }; } class Child extends Widget { inlineTemplate = "hey"; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(normalize(fixture.innerHTML)).toBe("
somediv
"); widget.state.flag = false; await nextTick(); expect(normalize(fixture.innerHTML)).toBe("
hey
"); }); }); 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 = `
txt
`; 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. class Parent extends Widget { inlineTemplate = `
`; widgets = { child: Child }; state = { flag: false }; } class Child extends Widget { inlineTemplate = `abcdef`; } const widget = new Parent(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
abc
"); widget.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
abcdef
"); }); test("snapshotting compiled code", async () => { env.qweb.addTemplate( "parent", `
` ); class Parent extends Widget { inlineTemplate = "parent"; widgets = { child: Child }; state = { flag: false }; } class Child extends Widget { inlineTemplate = `abcdef`; } const widget = new Parent(env); await widget.mount(fixture); expect(env.qweb.templates.parent.toString()).toMatchSnapshot(); }); test("t-props should not be undefined (snapshotting)", async () => { env.qweb.addTemplate("parent", `
`); class Parent extends Widget { inlineTemplate = "parent"; widgets = { child: Child }; } class Child extends Widget { inlineTemplate = `abc`; } const widget = new Parent(env); await widget.mount(fixture); expect(env.qweb.templates.parent.toString()).toMatchSnapshot(); }); }); describe("async rendering", () => { test("destroying a widget before start is over", async () => { let def = makeDeferred(); class W extends Widget { inlineTemplate = "invalid><"; willStart(): Promise { return def; } } const w = new W(env); w.mount(fixture); expect(w.__owl__.isDestroyed).toBe(false); expect(w.__owl__.isMounted).toBe(false); expect(w.__owl__.isStarted).toBe(false); w.destroy(); def.resolve(); await nextTick(); expect(w.__owl__.isDestroyed).toBe(true); expect(w.__owl__.isMounted).toBe(false); expect(w.__owl__.isStarted).toBe(false); }); test("destroying/recreating a subwidget with different props (if start is not over)", async () => { let def = makeDeferred(); let n = 0; class W extends Widget { inlineTemplate = `
`; widgets = { Child }; state = { val: 1 }; } class Child extends Widget { inlineTemplate = `child:`; constructor(parent, props) { super(parent, props); n++; } willStart(): Promise { return def; } } const w = new W(env); await w.mount(fixture); expect(n).toBe(0); w.state.val = 2; await nextMicroTick(); expect(n).toBe(1); w.state.val = 3; await nextMicroTick(); expect(n).toBe(2); def.resolve(); await nextTick(); expect(children(w).length).toBe(1); expect(fixture.innerHTML).toBe("
child:3
"); }); test("creating two async widgets, scenario 1", async () => { let defA = makeDeferred(); let defB = makeDeferred(); class ChildA extends Widget { inlineTemplate = "a"; willStart(): Promise { return defA; } } class ChildB extends Widget { inlineTemplate = "b"; willStart(): Promise { return defB; } } class Parent extends Widget { inlineTemplate = `
`; widgets = { ChildA, ChildB }; state = { flagA: false, flagB: false }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe("
"); parent.state.flagA = true; await nextTick(); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe("
"); parent.state.flagB = true; await nextTick(); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe("
"); defB.resolve(); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe("
"); defA.resolve(); await nextTick(); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe( "
ab
" ); }); test("creating two async widgets, scenario 2", async () => { let defA = makeDeferred(); let defB = makeDeferred(); class ChildA extends Widget { inlineTemplate = `a`; _updateProps(props, forceUpdate, fiber): Promise { return defA.then(() => super._updateProps(props, forceUpdate, fiber)); } } class ChildB extends Widget { inlineTemplate = `b`; willStart(): Promise { return defB; } } class Parent extends Widget { inlineTemplate = `
`; widgets = { ChildA, ChildB }; state = { valA: 1, valB: 2, flagB: false }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe( "
a1
" ); parent.state.valA = 2; await nextTick(); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe( "
a1
" ); parent.state.flagB = true; await nextTick(); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe( "
a1
" ); defB.resolve(); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe( "
a1
" ); defA.resolve(); await nextTick(); expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe( "
a2b2
" ); }); }); 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 () => { class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: Widget }; } const parent = new ParentWidget(env); await parent.mount(fixture); const child = children(parent)[0]; expect(child.env).toBe(parent.env); await child.updateEnv({ somekey: 4 }); expect(child.env).not.toBe(parent.env); expect((parent).env.somekey).toBeUndefined(); }); test("updating parent env does modify child env", async () => { class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: Widget }; } const parent = new ParentWidget(env); await parent.mount(fixture); const child = children(parent)[0]; expect(child.env.somekey).toBeUndefined(); await parent.updateEnv({ somekey: 4 }); expect(child.env.somekey).toBe(4); }); test("updating parent env does modify child env, part 2", async () => { class ParentWidget extends Widget { inlineTemplate = `
`; widgets = { child: Widget }; } const parent = new ParentWidget(env); await parent.mount(fixture); const child = children(parent)[0]; expect(child.env.somekey).toBeUndefined(); await child.updateEnv({ somekey: 4 }); await parent.updateEnv({ someotherkey: 4 }); expect(child.env.someotherkey).toBe(4); }); test("updating env force a rerender", async () => { class TestWidget extends Widget { inlineTemplate = `
`; } (env).someKey = "hey"; const widget = new TestWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
hey
"); await widget.updateEnv({ someKey: "rerendered" }); expect(fixture.innerHTML).toBe("
rerendered
"); }); test("updating env force rerendering children", async () => { class Parent extends Widget { inlineTemplate = `
`; widgets = { Child }; } class Child extends Widget { inlineTemplate = `
`; } (env).someKey = "hey"; const widget = new Parent(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
hey
"); await widget.updateEnv({ someKey: "rerendered" }); await nextTick(); expect(fixture.innerHTML).toBe("
rerendered
"); }); }); describe("widget and observable state", () => { test("widget is rerendered when its state is changed", async () => { class TestWidget extends Widget { state = { drink: "water" }; inlineTemplate = `
`; } 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); class Parent extends Widget { state = { obj: { coffee: 1 } }; widgets = { Child }; inlineTemplate = `
`; } 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 () => { class TestWidget extends Widget { state: any = { a: 1 }; inlineTemplate = `
`; } const widget = new TestWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
1
"); widget.set(widget.state, "b", 2); await nextTick(); expect(fixture.innerHTML).toBe("
12
"); }); });