import { Component, Env } from "../../src/component/component"; import { QWeb } from "../../src/qweb/qweb"; import { EventBus } from "../../src/core/event_bus"; import { makeDeferred, makeTestFixture, makeTestEnv, nextMicroTick, nextTick, normalize, editInput } 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 components let fixture: HTMLElement; let env: Env; beforeEach(() => { fixture = makeTestFixture(); env = makeTestEnv(); env.qweb.addTemplate("Widget", "
"); env.qweb.addTemplate( "Counter", `
` ); env.qweb.addTemplate("WidgetA", `
Hello
`); env.qweb.addTemplate("WidgetB", `
world
`); }); 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 components class Counter extends Widget { state = { counter: 0 }; inc() { this.state.counter++; } } class WidgetA extends Widget { components = { b: WidgetB }; } class WidgetB extends Widget {} //------------------------------------------------------------------------------ // 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("crashes if it cannot find a template", async () => { expect.assertions(1); class SomeWidget extends Component {} const widget = new SomeWidget(env); try { await widget.mount(fixture); } catch (e) { expect(e.message).toBe('Could not find template for component "SomeWidget"'); } }); test("can be clicked on and updated", async () => { const counter = new Counter(env); await counter.mount(fixture); expect(fixture.innerHTML).toBe("
0
"); const button = (counter.el).getElementsByTagName("button")[0]; await button.click(); await nextTick(); expect(fixture.innerHTML).toBe("
1
"); }); test("cannot be clicked on and updated if not in DOM", 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("
0
"); expect(counter.state.counter).toBe(1); }); test("widget style and classname", async () => { env.qweb.addTemplate( "StyledWidget", `
world
` ); class StyledWidget extends Widget {} 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; env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { 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 subcomponents, in proper order", async () => { const steps: any[] = []; env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { 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 subsubcomponents, in proper order", async () => { const steps: any[] = []; env.qweb.addTemplate( "ParentWidget", `
` ); env.qweb.addTemplate("ChildWidget", `
`); class ParentWidget extends Widget { components = { child: ChildWidget }; state = { flag: false }; mounted() { steps.push("parent:mounted"); } willUnmount() { steps.push("parent:willUnmount"); } } class ChildWidget extends Widget { components = { 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 subsubcomponents, in proper order", async () => { const steps: any[] = []; env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { child: ChildWidget }; state = { n: 1 }; willPatch() { steps.push("parent:willPatch"); } patched() { steps.push("parent:patched"); } } env.qweb.addTemplate("ChildWidget", `
`); class ChildWidget extends Widget { components = { childchild: ChildChildWidget }; willPatch() { steps.push("child:willPatch"); } patched() { steps.push("child:patched"); } } env.qweb.addTemplate("ChildChildWidget", `
`); class ChildChildWidget extends Widget { 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 env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { state = { ok: false }; components = { 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 subcomponents 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("components are unmounted and destroyed if no longer in DOM", async () => { let steps: string[] = []; env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { state = { ok: true }; components = { 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("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => { let childUnmounted = false; env.qweb.addTemplate("ChildWidget", ``); class ChildWidget extends Widget { willUnmount() { childUnmounted = true; } increment() { this.state += 1; } } env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { ChildWidget }; 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[] = []; env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { 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(); env.qweb.addTemplate("Parent", ''); class Parent extends Widget { state = { n: 1 }; components = { Child: HookWidget }; } env.qweb.addTemplate("HookWidget", ''); class HookWidget extends Widget { 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; env.qweb.addTemplate("Parent", '
'); class Parent extends Widget { state = { a: 1 }; components = { 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; env.qweb.addTemplate("Parent", `
`); class Parent extends Widget { state = { val: 42 }; components = { Child: TestWidget }; } env.qweb.addTemplate("TestWidget", `
`); class TestWidget extends Widget { 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; env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { 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[] = []; env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { 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[] = []; env.qweb.addTemplates(`
`); class ParentWidget extends Widget { components = { 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(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); 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__.isMounted).toBe(false); expect(widget.__owl__.isDestroyed).toBe(false); widget.destroy(); expect(widget.__owl__.isMounted).toBe(false); expect(widget.__owl__.isDestroyed).toBe(true); def.resolve(); await nextTick(); 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("can use components from the global registry", async () => { QWeb.register("WidgetB", WidgetB); env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
world
"); delete QWeb.components["WidgetB"]; }); test("don't fallback to global registry if widget defined locally", async () => { QWeb.register("WidgetB", WidgetB); // should not use this widget env.qweb.addTemplate("ParentWidget", `
`); env.qweb.addTemplate("AnotherWidgetB", `Belgium`); class AnotherWidgetB extends Widget {} class ParentWidget extends Widget { components = { WidgetB: AnotherWidgetB }; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
Belgium
"); delete QWeb.components["WidgetB"]; }); test("can define components in template without t-component", async () => { env.qweb.addTemplates(`
`); class C extends Widget {} class P extends Widget { components = { C }; } const parent = new P(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
1
"); }); test("display a nice error if it cannot find component", async () => { const consoleError = console.error; console.error = jest.fn(); env.qweb.addTemplate("Parent", `
`); class Parent extends Widget { components = { SomeWidget: Widget }; } const parent = new Parent(env); await parent.mount(fixture); expect(console.error).toBeCalledTimes(1); expect((console.error).mock.calls[0][0].message).toMatch( 'Cannot find the definition of component "SomeMispelledWidget"' ); console.error = consoleError; }); test("t-refs on widget are components", async () => { env.qweb.addTemplate("WidgetC", `
Hello
`); class WidgetC extends Widget { components = { 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); env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { 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); env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { 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 () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { 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("refs in a loop", async () => { env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { state = { items: [1, 2, 3] }; components = { Child: Widget }; } const parent = new ParentWidget(env); await parent.mount(fixture); expect(Object.keys(parent.refs)).toEqual(["1", "2", "3"]); }); 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 components", 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 () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { 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 components are destroyed if no longer in dom, then recreated", async () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { state = { ok: true }; components = { 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 components with t-keepalive are not destroyed if no longer in dom", async () => { env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { state = { ok: true }; components = { 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 components dom state with t-keepalive is preserved", async () => { env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { state = { ok: true }; components = { InputWidget }; } env.qweb.addTemplate("InputWidget", ""); class InputWidget extends Widget {} 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"); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("sub widget with t-ref and t-keepalive", async () => { env.qweb.addTemplates(`
Hello
`); class ParentWidget extends Widget { state = { ok: true }; components = { ChildWidget }; } class ChildWidget extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); let child = children(widget)[0]; expect(fixture.innerHTML).toBe("
Hello
"); expect(widget.refs.child).toEqual(child); widget.state.ok = false; await nextTick(); expect(fixture.innerHTML).toBe("
"); expect(widget.refs.child).toEqual(child); widget.state.ok = true; await nextTick(); expect(fixture.innerHTML).toBe("
Hello
"); expect(widget.refs.child).toEqual(child); }); test("sub components rendered in a loop", async () => { env.qweb.addTemplate("ChildWidget", ``); class ChildWidget extends Widget {} env.qweb.addTemplate( "Parent", `
` ); class Parent extends Widget { state = { numbers: [1, 2, 3] }; components = { ChildWidget }; } const parent = new Parent(env); await parent.mount(fixture); expect(normalize(fixture.innerHTML)).toBe( normalize(`
1 2 3
`) ); }); test("sub components with some state rendered in a loop", async () => { let n = 1; env.qweb.addTemplate("ChildWidget", ``); class ChildWidget extends Widget { constructor(parent) { super(parent); this.state = { n }; n++; } } env.qweb.addTemplate( "parent", `
` ); class Parent extends Widget { template = "parent"; state = { numbers: [1, 2, 3] }; components = { 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.fn.toString()).toMatchSnapshot(); }); test("sub components between t-ifs", async () => { // this confuses the patching algorithm... env.qweb.addTemplate("ChildWidget", `child`); class ChildWidget extends Widget {} env.qweb.addTemplate( "Parent", `

hey

noo

test
` ); class Parent extends Widget { state = { flag: false }; components = { 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
`) ); }); test("t-component with dynamic value", async () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { WidgetB }; state = { widget: "WidgetB" }; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
world
"); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-component with dynamic value 2", async () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { WidgetB }; state = { widget: "B" }; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
world
"); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); }); describe("props evaluation ", () => { test("explicit object prop", async () => { env.qweb.addTemplate("Parent", `
`); class Parent extends Widget { components = { child: Child }; state = { val: 42 }; } env.qweb.addTemplate("Child", ``); class Child extends Widget { 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("accept ES6-like syntax for props (with getters)", async () => { env.qweb.addTemplate("Child", ``); class Child extends Widget {} env.qweb.addTemplate("Parent", `
`); class Parent extends Widget { components = { 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 ", async () => { env.qweb.addTemplate( "Parent", `
` ); class Parent extends Widget { components = { child: Child }; } env.qweb.addTemplate( "Child", ` ` ); class Child extends Widget {} const widget = new Parent(env); await widget.mount(fixture); expect(normalize(fixture.innerHTML)).toBe("
42
"); }); }); describe("class and style attributes with t-component", () => { test("class is properly added on widget root el", async () => { env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { child: Child }; } env.qweb.addTemplate("Child", `
`); class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); }); test("t-att-class is properly added/removed on widget root el", async () => { env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { child: Child }; state = { a: true, b: false }; } env.qweb.addTemplate("Child", `
`); class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); widget.state.a = false; widget.state.b = true; await nextTick(); expect(fixture.innerHTML).toBe(`
`); }); test("class with extra whitespaces", async () => { env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { Child }; } env.qweb.addTemplate("Child", `
`); class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); }); test("t-att-class is properly added/removed on widget root el (v2)", async () => { env.qweb.addTemplates(`
`); class ParentWidget extends Widget { components = { Child }; state = { b: true }; } class Child extends Widget { state = { d: true }; } const widget = new ParentWidget(env); await widget.mount(fixture); const span = fixture.querySelector("span")!; expect(span.className).toBe("c d a b"); widget.state.b = false; await nextTick(); expect(span.className).toBe("c d a"); (widget.refs.child).state.d = false; await nextTick(); expect(span.className).toBe("c a"); widget.state.b = true; await nextTick(); expect(span.className).toBe("c a b"); (widget.refs.child).state.d = true; await nextTick(); expect(span.className).toBe("c a b d"); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot(); }); test("t-att-class is properly added/removed on widget root el (v3)", async () => { env.qweb.addTemplates(`
`); class ParentWidget extends Widget { components = { Child }; state = { b: true }; } class Child extends Widget { state = { d: true }; } const widget = new ParentWidget(env); await widget.mount(fixture); const span = fixture.querySelector("span")!; expect(span.className).toBe("c d a b"); widget.state.b = false; await nextTick(); expect(span.className).toBe("c d a"); (widget.refs.child).state.d = false; await nextTick(); expect(span.className).toBe("c a"); widget.state.b = true; await nextTick(); expect(span.className).toBe("c a b"); (widget.refs.child).state.d = true; await nextTick(); expect(span.className).toBe("c a b d"); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot(); }); test("class on components do not interfere with user defined classes", async () => { env.qweb.addTemplates(`
`); class App extends Widget { state = { c: true }; mounted() { this.el!.classList.add("user"); } } const widget = new App(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe('
'); widget.state.c = false; await nextTick(); expect(fixture.innerHTML).toBe('
'); }); test("style is properly added on widget root el", async () => { env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { child: Widget }; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); }); test("dynamic t-att-style is properly added and updated on widget root el", async () => { env.qweb.addTemplate( "ParentWidget", `
` ); class ParentWidget extends Widget { components = { child: Widget }; state = { style: "font-size: 20px" }; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); expect(fixture.innerHTML).toBe(`
`); widget.state.style = "font-size: 30px"; await nextTick(); expect(fixture.innerHTML).toBe(`
`); }); }); describe("other directives with t-component", () => { test("t-on works as expected", async () => { expect.assertions(4); env.qweb.addTemplates(`
`); class ParentWidget extends Widget { components = { child: Child }; n = 0; someMethod(ev) { expect(ev.detail).toBe(43); this.n++; } } class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); let child = children(widget)[0]; expect(widget.n).toBe(0); child.trigger("custom-event", 43); expect(widget.n).toBe(1); child.destroy(); child.trigger("custom-event", 43); expect(widget.n).toBe(1); }); test("t-on with handler bound to argument", async () => { expect.assertions(3); env.qweb.addTemplates(`
`); class ParentWidget extends Widget { components = { child: Child }; onEv(n, ev) { expect(n).toBe(3); expect(ev.detail).toBe(43); } } class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); let child = children(widget)[0]; child.trigger("ev", 43); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-on with handler bound to object", async () => { expect.assertions(3); env.qweb.addTemplates(`
`); class ParentWidget extends Widget { components = { child: Child }; onEv(o, ev) { expect(o).toEqual({ val: 3 }); expect(ev.detail).toBe(43); } } class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); let child = children(widget)[0]; child.trigger("ev", 43); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-on with handler bound to empty object", async () => { expect.assertions(3); env.qweb.addTemplates(`
`); class ParentWidget extends Widget { components = { child: Child }; onEv(o, ev) { expect(o).toEqual({}); expect(ev.detail).toBe(43); } } class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); let child = children(widget)[0]; child.trigger("ev", 43); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-on with handler bound to empty object (with non empty inner string)", async () => { expect.assertions(3); env.qweb.addTemplates(`
`); class ParentWidget extends Widget { components = { child: Child }; onEv(o, ev) { expect(o).toEqual({}); expect(ev.detail).toBe(43); } } class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); let child = children(widget)[0]; child.trigger("ev", 43); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-on with stop and/or prevent modifiers", async () => { expect.assertions(7); env.qweb.addTemplates(`
`); class ParentWidget extends Widget { components = { child: Child }; onEv1(ev) { expect(ev.defaultPrevented).toBe(false); expect(ev.cancelBubble).toBe(true); } onEv2(ev) { expect(ev.defaultPrevented).toBe(true); expect(ev.cancelBubble).toBe(false); } onEv3(ev) { expect(ev.defaultPrevented).toBe(true); expect(ev.cancelBubble).toBe(true); } } class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); const child = children(widget)[0]; child.trigger("ev-1"); child.trigger("ev-2"); child.trigger("ev-3"); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-on with self modifier", async () => { env.qweb.addTemplates(`
`); const steps: string[] = []; class ParentWidget extends Widget { components = { child: Child }; onEv1(ev) { steps.push("onEv1"); } onEv2(ev) { steps.push("onEv2"); } } class Child extends Widget { components = { child: GrandChild }; } class GrandChild extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); const child = children(widget)[0]; const grandChild = children(child)[0]; child.trigger("ev-1"); child.trigger("ev-2"); expect(steps).toEqual(["onEv1", "onEv2"]); grandChild.trigger("ev-1"); grandChild.trigger("ev-2"); expect(steps).toEqual(["onEv1", "onEv2", "onEv1"]); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-on with self and prevent modifiers (order matters)", async () => { env.qweb.addTemplates(`
`); const steps: boolean[] = []; class ParentWidget extends Widget { components = { child: Child }; onEv() {} } class Child extends Widget { components = { child: GrandChild }; } class GrandChild extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); (fixture).addEventListener("ev", function(e) { steps.push(e.defaultPrevented); }); const child = children(widget)[0]; const grandChild = children(child)[0]; child.trigger("ev"); grandChild.trigger("ev"); expect(steps).toEqual([true, false]); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-on with prevent and self modifiers (order matters)", async () => { env.qweb.addTemplates(`
`); const steps: boolean[] = []; class ParentWidget extends Widget { components = { Child }; onEv() {} } class Child extends Widget { components = { GrandChild }; } class GrandChild extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); (fixture).addEventListener("ev", function(e) { steps.push(e.defaultPrevented); }); const child = children(widget)[0]; const grandChild = children(child)[0]; child.trigger("ev"); grandChild.trigger("ev"); expect(steps).toEqual([true, true]); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-if works with t-component", async () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { child: Child }; state = { flag: true }; } env.qweb.addTemplate("Child", "hey"); class Child extends Widget {} 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-component", async () => { env.qweb.addTemplate( "ParentWidget", `
somediv
` ); class ParentWidget extends Widget { components = { child: Child }; state = { flag: true }; } env.qweb.addTemplate("Child", "hey"); class Child extends Widget {} 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-elif works with t-component", async () => { env.qweb.addTemplate( "ParentWidget", `
somediv
` ); class ParentWidget extends Widget { components = { child: Child }; state = { flag: true }; } env.qweb.addTemplate("Child", "hey"); class Child extends Widget {} 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-component", async () => { env.qweb.addTemplate( "ParentWidget", `
somediv
` ); class ParentWidget extends Widget { components = { child: Child }; state = { flag: true }; } env.qweb.addTemplate("Child", "hey"); class Child extends Widget {} 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-component // directive as a key env.qweb.addTemplate( "Test", `
txt
` ); class Test extends Widget { components = { widget: Widget }; } const widget = new Test(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
txttxt
"); }); test("t-on with handler bound to dynamic argument on a t-foreach", async () => { expect.assertions(3); env.qweb.addTemplates(`
`); const items = [1, 2, 3, 4]; class ParentWidget extends Widget { components = { Child }; onEv(n, ev) { expect(n).toBe(1); expect(ev.detail).toBe(43); } } class Child extends Widget {} const widget = new ParentWidget(env, { items }); await widget.mount(fixture); children(widget)[0].trigger("ev", 43); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("updating widget immediately", async () => { // in this situation, we protect against a bug that occurred: because of the // interplay between components and vnodes, a sub widget vnode was patched // twice. env.qweb.addTemplate("Parent", `
`); class Parent extends Widget { components = { child: Child }; state = { flag: false }; } env.qweb.addTemplate("Child", `abcdef`); class Child extends Widget {} 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 { components = { child: Child }; state = { flag: false }; } env.qweb.addTemplate("Child", `abcdef`); class Child extends Widget {} const widget = new Parent(env); await widget.mount(fixture); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); }); test("component semantics", async () => { let steps: string[] = []; let c: C; class TestWidget extends Widget { name: string = "test"; async willStart() { steps.push(`${this.name}:willStart`); } __render(f, p) { steps.push(`${this.name}:render`); return super.__render(f, p); } __patch(vnode) { steps.push(`${this.name}:__patch`); super.__patch(vnode); } __mount(vnode, elm) { steps.push(`${this.name}:__patch(from __mount)`); return super.__mount(vnode, elm); } mounted() { steps.push(`${this.name}:mounted`); } async willUpdateProps() { steps.push(`${this.name}:willUpdateProps`); } willPatch() { steps.push(`${this.name}:willPatch`); } patched() { steps.push(`${this.name}:patched`); } willUnmount() { steps.push(`${this.name}:willUnmount`); } destroy() { super.destroy(); steps.push(`${this.name}:destroy`); } } env.qweb.addTemplate("A", `
A
`); class A extends TestWidget { components = { B, C }; name = "A"; } env.qweb.addTemplate("B", `
B
`); class B extends TestWidget { name = "B"; constructor(parent, props) { super(parent, props); steps.push("B:constructor"); } } env.qweb.addTemplate( "C", `
C
` ); class C extends TestWidget { components = { D, E, F }; name = "C"; state = { flag: true }; constructor(parent, props) { super(parent, props); c = this; steps.push("C:constructor"); } } env.qweb.addTemplate("D", `
D
`); class D extends TestWidget { name = "D"; constructor(parent, props) { super(parent, props); steps.push("D:constructor"); } } env.qweb.addTemplate("E", `
E
`); class E extends TestWidget { name = "E"; constructor(parent, props) { super(parent, props); steps.push("E:constructor"); } } env.qweb.addTemplate("F", `
F
`); class F extends TestWidget { name = "F"; constructor(parent, props) { super(parent, props); steps.push("F:constructor"); } } const a = new A(env); await a.mount(fixture); expect(fixture.innerHTML).toBe(`
A
B
C
D
E
`); expect(steps).toEqual([ "A:willStart", "A:render", "B:constructor", "B:willStart", "C:constructor", "C:willStart", "B:render", "C:render", "D:constructor", "D:willStart", "E:constructor", "E:willStart", "D:render", "E:render", "A:__patch", "B:__patch(from __mount)", "C:__patch(from __mount)", "D:__patch(from __mount)", "E:__patch(from __mount)", "B:mounted", "D:mounted", "E:mounted", "C:mounted", "A:mounted" ]); // update steps = []; c!.state.flag = false; await nextTick(); expect(steps).toEqual([ "C:render", "D:willUpdateProps", "F:constructor", "F:willStart", "D:render", "F:render", "C:willPatch", "D:willPatch", "C:__patch", "E:willUnmount", "E:destroy", "F:__patch(from __mount)", "F:mounted", "D:__patch", "D:patched", "C:patched" ]); }); }); describe("async rendering", () => { test("destroying a widget before start is over", async () => { let def = makeDeferred(); class W extends Widget { willStart(): Promise { return def; } } const w = new W(env); w.mount(fixture); expect(w.__owl__.isDestroyed).toBe(false); expect(w.__owl__.isMounted).toBe(false); w.destroy(); def.resolve(); await nextTick(); expect(w.__owl__.isDestroyed).toBe(true); expect(w.__owl__.isMounted).toBe(false); }); test("destroying/recreating a subwidget with different props (if start is not over)", async () => { let def = makeDeferred(); let n = 0; env.qweb.addTemplate("W", `
`); class W extends Widget { components = { Child }; state = { val: 1 }; } env.qweb.addTemplate("Child", `child:`); class Child extends Widget { 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 components, scenario 1", async () => { let defA = makeDeferred(); let defB = makeDeferred(); env.qweb.addTemplate("ChildA", "a"); class ChildA extends Widget { willStart(): Promise { return defA; } } env.qweb.addTemplate("ChildB", "b"); class ChildB extends Widget { willStart(): Promise { return defB; } } env.qweb.addTemplate( "Parent", `
` ); class Parent extends Widget { components = { 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 components, scenario 2", async () => { let defA = makeDeferred(); let defB = makeDeferred(); env.qweb.addTemplate("ChildA", `a`); class ChildA extends Widget { __updateProps(props, forceUpdate, fiber): Promise { return defA.then(() => super.__updateProps(props, forceUpdate, fiber)); } } env.qweb.addTemplate("ChildB", `b`); class ChildB extends Widget { willStart(): Promise { return defB; } } env.qweb.addTemplate( "Parent", `
` ); class Parent extends Widget { components = { 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
" ); }); test("components in a node in a t-foreach ", async () => { class Child extends Widget {} class App extends Widget { components = { Child }; get items() { return [1, 2]; } } env.qweb.addTemplates(`
`); const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe( "
  • 1
  • 2
" ); }); test("properly behave when destroyed/unmounted while rendering ", async () => { let def = Promise.resolve(); env.qweb.addTemplate("Child", `
`); class Child extends Widget { components = { SubChild }; mounted() { // from now on, each rendering in child widget will be delayed (see // __render) def = makeDeferred(); } async __render(f, p) { const result = await super.__render(f, p); await def; return result; } } class SubChild extends Widget { willPatch() { throw new Error("Should not happen!"); } patched() { throw new Error("Should not happen!"); } } env.qweb.addTemplate( "Parent", `
` ); class Parent extends Widget { components = { Child }; state = { flag: true, val: "Framboise Lindemans" }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
"); // this change triggers a rendering of the parent. This rendering is delayed, // because child is now waiting for def to be resolved parent.state.val = "Framboise Girardin"; await nextTick(); // with this, we remove child, and childchild, even though it is not finished // rendering from previous changes parent.state.flag = false; await nextTick(); // we now resolve def, so the child rendering is now complete. (def).resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
"); }); test("reuse widget if possible, in some async situation", async () => { env.qweb.addTemplates(` a b `); let destroyCount = 0; class ChildA extends Widget { destroy() { destroyCount++; super.destroy(); } } class ChildB extends Widget { willStart(): any { return new Promise(function() {}); } } class Parent extends Widget { components = { ChildA, ChildB }; state = { valA: 1, valB: 2, flag: false }; } const parent = new Parent(env); await parent.mount(fixture); expect(destroyCount).toBe(0); parent.state.flag = true; await nextTick(); expect(destroyCount).toBe(0); parent.state.valB = 3; await nextTick(); expect(destroyCount).toBe(0); }); test("delayed component with t-asyncroot directive", async () => { env.qweb.addTemplates(`
`); let def; class Parent extends Widget { components = { Child, AsyncChild }; state = { val: 0 }; updateApp() { this.state.val++; } } class Child extends Widget {} class AsyncChild extends Child { willUpdateProps() { return def; } } const parent = new Parent(env); await parent.mount(fixture); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); expect(fixture.querySelector(".children")!.innerHTML).toBe("00"); // click on button to increment Parent counter def = makeDeferred(); fixture.querySelector("button")!.click(); await nextTick(); expect(fixture.querySelector(".children")!.innerHTML).toBe("10"); def.resolve(); await nextTick(); expect(fixture.querySelector(".children")!.innerHTML).toBe("11"); }); test("fast component with t-asyncroot directive", async () => { env.qweb.addTemplates(`
`); let def; class Parent extends Widget { components = { Child, AsyncChild }; state = { val: 0 }; updateApp() { this.state.val++; } } class Child extends Widget {} class AsyncChild extends Child { willUpdateProps() { return def; } } const parent = new Parent(env); await parent.mount(fixture); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); expect(fixture.querySelector(".children")!.innerHTML).toBe("00"); // click on button to increment Parent counter def = makeDeferred(); fixture.querySelector("button")!.click(); await nextTick(); expect(fixture.querySelector(".children")!.innerHTML).toBe("10"); def.resolve(); await nextTick(); expect(fixture.querySelector(".children")!.innerHTML).toBe("11"); }); test("t-component with t-asyncroot directive: mixed re-renderings", async () => { env.qweb.addTemplates(`
/
`); let def; class Parent extends Widget { components = { Child, AsyncChild }; state = { val: 0 }; updateApp() { this.state.val++; } } class Child extends Widget { state = { val: 0 }; increment() { this.state.val++; } } class AsyncChild extends Child { willUpdateProps() { return def; } } const parent = new Parent(env); await parent.mount(fixture); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); expect(fixture.querySelector(".children")!.innerHTML).toBe("0/00/0"); // click on button to increment Parent counter def = makeDeferred(); fixture.querySelector("button")!.click(); await nextTick(); expect(fixture.querySelector(".children")!.innerHTML).toBe("0/10/0"); // click on each Child to increment their local counter const children = parent.el!.querySelectorAll("span"); children[0]!.click(); await nextTick(); expect(fixture.querySelector(".children")!.innerHTML).toBe("1/10/0"); children[1]!.click(); await nextTick(); expect(fixture.querySelector(".children")!.innerHTML).toBe("1/11/0"); // finalize first re-rendering (coming from the props update) def.resolve(); await nextTick(); expect(fixture.querySelector(".children")!.innerHTML).toBe("1/11/1"); }); test("rendering component again in next microtick", async () => { class Child extends Widget {} class App extends Widget { components = { Child }; async onClick() { (env as any).flag = true; this.render(); await Promise.resolve(); this.render(); } } env.qweb.addTemplates(`
Child
`); const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
"); fixture.querySelector("button")!.click(); await nextTick(); expect(fixture.innerHTML).toBe("
Child
"); }); }); 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 () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { 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 () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { 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 () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Widget { components = { 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 () => { env.qweb.addTemplate("TestWidget", `
`); class TestWidget extends Widget {} (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 () => { env.qweb.addTemplate("Parent", `
`); class Parent extends Widget { components = { Child }; } env.qweb.addTemplate("Child", `
`); class Child extends Widget {} (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 () => { env.qweb.addTemplate("TestWidget", `
`); class TestWidget extends Widget { state = { drink: "water" }; } 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("subcomponents cannot change observable state received from parent", async () => { const consoleError = console.error; console.error = jest.fn(); env.qweb.addTemplate("Parent", `
`); class Parent extends Widget { state = { obj: { coffee: 1 } }; components = { Child }; } class Child extends Widget { constructor(parent, props) { super(parent, props); props.obj.coffee = 2; } } const parent = new Parent(env); await parent.mount(fixture); expect(console.error).toBeCalledTimes(1); expect((console.error).mock.calls[0][0].message).toMatch( 'Observed state cannot be changed here! (key: "coffee", val: "2")' ); console.error = consoleError; }); }); describe("t-mounted directive", () => { test("callback is not called when not in DOM", async () => { env.qweb.addTemplate("TestWidget", `
`); class TestWidget extends Widget { f() {} } const widget = new TestWidget(env); widget.f = jest.fn(); await widget.mount(document.createElement("div")); expect(widget.f).toHaveBeenCalledTimes(0); }); test("callback is called when in DOM", async () => { env.qweb.addTemplate("TestWidget", `
`); class TestWidget extends Widget { f() {} } const widget = new TestWidget(env); widget.f = jest.fn(); await widget.mount(fixture); expect(widget.f).toHaveBeenCalledTimes(1); }); test("callback with args is called when in DOM", async () => { env.qweb.addTemplate("TestWidget", `
`); class TestWidget extends Widget { f() {} } const widget = new TestWidget(env); widget.f = jest.fn(); await widget.mount(fixture); expect(widget.f).toHaveBeenCalledTimes(1); expect(widget.f).toHaveBeenCalledWith(2); }); test("combined with a t-if", async () => { env.qweb.addTemplate("TestWidget", `
`); class TestWidget extends Widget { state = { flag: false }; f() {} } const widget = new TestWidget(env); widget.f = jest.fn(); await widget.mount(fixture); expect(widget.f).toHaveBeenCalledTimes(0); widget.state.flag = true; await nextTick(); expect(widget.f).toHaveBeenCalledTimes(1); }); test("combined with a t-ref", async () => { env.qweb.addTemplate("TestWidget", `
`); class TestWidget extends Widget { f() {} } const widget = new TestWidget(env); widget.f = jest.fn(); await widget.mount(fixture); expect(widget.refs.input).toBeDefined(); expect(widget.f).toHaveBeenCalledTimes(1); }); }); describe("can deduce template from name", () => { test("can find template if name of component", async () => { class ABC extends Widget {} env.qweb.addTemplate("ABC", "Orval"); const abc = new ABC(env); await abc.mount(fixture); expect(fixture.innerHTML).toBe("Orval"); }); test("can find template of parent component", async () => { class ABC extends Widget {} class DEF extends ABC {} env.qweb.addTemplate("ABC", "Orval"); const def = new DEF(env); await def.mount(fixture); expect(fixture.innerHTML).toBe("Orval"); }); test("can find template of parent component, defined by template key", async () => { class ABC extends Widget { template = "Achel"; } class DEF extends ABC {} env.qweb.addTemplate("Achel", "Orval"); const def = new DEF(env); await def.mount(fixture); expect(fixture.innerHTML).toBe("Orval"); }); test("templates are found in proper qweb instance", async () => { const env2 = makeTestEnv(); env.qweb.addTemplate("ABC", "Rochefort 8"); env2.qweb.addTemplate("ABC", "Rochefort 10"); class ABC extends Widget {} const abc = new ABC(env); await abc.mount(fixture); expect(fixture.innerHTML).toBe("Rochefort 8"); abc.destroy(); const abc2 = new ABC(env2); await abc2.mount(fixture); expect(fixture.innerHTML).toBe("Rochefort 10"); }); }); describe("t-slot directive", () => { test("can define and call slots", async () => { env.qweb.addTemplates(`
header footer
`); class Dialog extends Widget {} class Parent extends Widget { components = { Dialog }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe( "
header
footer
" ); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot(); }); test("slots are rendered with proper context", async () => { env.qweb.addTemplates(`
`); class Dialog extends Widget {} class Parent extends Widget { components = { Dialog }; state = { val: 0 }; doSomething() { this.state.val++; } } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe( '
0
' ); fixture.querySelector("button")!.click(); await nextTick(); expect(fixture.innerHTML).toBe( '
1
' ); }); test("slots are rendered with proper context, part 2", async () => { env.qweb.addTemplates(`
  • User
  • `); class Link extends Widget {} class App extends Widget { state = { users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] }; components = { Link }; } const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe( '' ); expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot(); // test updateprops here app.state.users[1].name = "Mathieu"; await nextTick(); expect(fixture.innerHTML).toBe( '' ); }); test("slots are rendered with proper context, part 3", async () => { env.qweb.addTemplates(`
  • `); class Link extends Widget {} class App extends Widget { state = { users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] }; components = { Link }; } const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe( '' ); expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot(); // test updateprops here app.state.users[1].name = "Mathieu"; await nextTick(); expect(fixture.innerHTML).toBe( '' ); }); test("slots are rendered with proper context, part 4", async () => { env.qweb.addTemplates(`
    `); class Link extends Widget {} class App extends Widget { state = { user: { id: 1, name: "Aaron" } }; components = { Link }; } const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe(''); expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot(); // test updateprops here app.state.user.name = "David"; await nextTick(); expect(fixture.innerHTML).toBe(''); }); test("refs are properly bound in slots", async () => { env.qweb.addTemplates(`
    `); class Dialog extends Widget {} class Parent extends Widget { components = { Dialog }; state = { val: 0 }; doSomething() { this.state.val++; } } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe( '
    0
    ' ); (parent.refs.myButton).click(); await nextTick(); expect(fixture.innerHTML).toBe( '
    1
    ' ); }); test("content is the default slot", async () => { env.qweb.addTemplates(`
    sts rocks
    `); class Dialog extends Widget {} class Parent extends Widget { components = { Dialog }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    sts rocks
    "); }); test("default slot work with text nodes", async () => { env.qweb.addTemplates(`
    sts rocks
    `); class Dialog extends Widget {} class Parent extends Widget { components = { Dialog }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    sts rocks
    "); }); test("multiple roots are allowed in a named slot", async () => { env.qweb.addTemplates(`
    sts rocks
    `); class Dialog extends Widget {} class Parent extends Widget { components = { Dialog }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    stsrocks
    "); }); test("multiple roots are allowed in a default slot", async () => { env.qweb.addTemplates(`
    sts rocks
    `); class Dialog extends Widget {} class Parent extends Widget { components = { Dialog }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    stsrocks
    "); }); test("missing slots are ignored", async () => { env.qweb.addTemplates(`
    some content
    `); class Dialog extends Widget {} class Parent extends Widget { components = { Dialog }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    some content
    "); }); test("t-debug on a t-set (defining a slot)", async () => { const consoleLog = console.log; console.log = jest.fn(); env.qweb.addTemplates(`
    abc
    `); class Dialog extends Widget {} class Parent extends Widget { components = { Dialog }; } const parent = new Parent(env); await parent.mount(fixture); expect(console.log).toHaveBeenCalledTimes(0); console.log = consoleLog; }); test("slot preserves properly parented relationship", async () => { env.qweb.addTemplates(`
    Grand Child
    `); class Child extends Widget {} class GrandChild extends Widget {} class Parent extends Widget { components = { Child, GrandChild }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    Grand Child
    "); const parentChildren = children(parent); expect(parentChildren.length).toBe(1); expect(parentChildren[0]).toBeInstanceOf(Child); const childrenChildren = children(parentChildren[0]); expect(childrenChildren.length).toBe(1); expect(childrenChildren[0]).toBeInstanceOf(GrandChild); }); test("slot are properly rendered if inner props are changed", async () => { env.qweb.addTemplates(`
    SC:
    `); class SomeComponent extends Widget {} class GenericComponent extends Widget {} class App extends Widget { components = { GenericComponent, SomeComponent }; state = { val: 4 }; inc() { this.state.val++; } } const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
    SC:4
    "); (fixture.querySelector("button")).click(); await nextTick(); expect(fixture.innerHTML).toBe("
    SC:5
    "); }); }); describe("t-model directive", () => { test("basic use, on an input", async () => { env.qweb.addTemplates(`
    `); class SomeComponent extends Widget { state = { text: "" }; } const comp = new SomeComponent(env); await comp.mount(fixture); expect(fixture.innerHTML).toBe("
    "); const input = fixture.querySelector("input")!; await editInput(input, "test"); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
    test
    "); expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); }); test("on an input, type=checkbox", async () => { env.qweb.addTemplates(`
    yes no
    `); class SomeComponent extends Widget { state = { flag: false }; } const comp = new SomeComponent(env); await comp.mount(fixture); expect(fixture.innerHTML).toBe('
    no
    '); let input = fixture.querySelector("input")!; input.click(); await nextTick(); expect(fixture.innerHTML).toBe('
    yes
    '); expect(comp.state.flag).toBe(true); expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); input.click(); await nextTick(); expect(comp.state.flag).toBe(false); }); test("on an textarea", async () => { env.qweb.addTemplates(`
    "); const textarea = fixture.querySelector("textarea")!; await editInput(textarea, "test"); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
    test
    "); }); test("on an input type=radio", async () => { env.qweb.addTemplates(`
    Choice:
    `); class SomeComponent extends Widget { state = { choice: "" }; } const comp = new SomeComponent(env); await comp.mount(fixture); expect(fixture.innerHTML).toBe( '
    Choice:
    ' ); const firstInput = fixture.querySelector("input")!; firstInput.click(); await nextTick(); expect(comp.state.choice).toBe("One"); expect(fixture.innerHTML).toBe( '
    Choice: One
    ' ); const secondInput = fixture.querySelectorAll("input")[1]; secondInput.click(); await nextTick(); expect(comp.state.choice).toBe("Two"); expect(fixture.innerHTML).toBe( '
    Choice: Two
    ' ); expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); }); test("on a select", async () => { env.qweb.addTemplates(`
    Choice:
    `); class SomeComponent extends Widget { state = { color: "" }; } const comp = new SomeComponent(env); await comp.mount(fixture); expect(fixture.innerHTML).toBe( '
    Choice:
    ' ); const select = fixture.querySelector("select")!; select.value = "red"; select.dispatchEvent(new Event("change")); await nextTick(); expect(comp.state.color).toBe("red"); expect(fixture.innerHTML).toBe( '
    Choice: red
    ' ); expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); }); test(".lazy modifier", async () => { env.qweb.addTemplates(`
    `); class SomeComponent extends Widget { state = { text: "" }; } const comp = new SomeComponent(env); await comp.mount(fixture); expect(fixture.innerHTML).toBe("
    "); const input = fixture.querySelector("input")!; input.value = "test"; input.dispatchEvent(new Event("input")); await nextTick(); expect(comp.state.text).toBe(""); expect(fixture.innerHTML).toBe("
    "); input.dispatchEvent(new Event("change")); await nextTick(); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
    test
    "); expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); }); test(".trim modifier", async () => { env.qweb.addTemplates(`
    `); class SomeComponent extends Widget { state = { text: "" }; } const comp = new SomeComponent(env); await comp.mount(fixture); const input = fixture.querySelector("input")!; await editInput(input, " test "); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
    test
    "); }); test(".number modifier", async () => { env.qweb.addTemplates(`
    `); class SomeComponent extends Widget { state = { number: 0 }; } const comp = new SomeComponent(env); await comp.mount(fixture); expect(fixture.innerHTML).toBe("
    0
    "); const input = fixture.querySelector("input")!; await editInput(input, "13"); expect(comp.state.number).toBe(13); expect(fixture.innerHTML).toBe("
    13
    "); await editInput(input, "invalid"); expect(comp.state.number).toBe("invalid"); expect(fixture.innerHTML).toBe("
    invalid
    "); }); }); describe("environment and plugins", () => { // some source of external events let bus = new EventBus(); // definition of a plugin const somePlugin = env => { env.someFlag = true; bus.on("some-event", null, () => { env.someFlag = !env.someFlag; env.qweb.forceUpdate(); }); }; test("plugin works as expected", async () => { somePlugin(env); env.qweb.addTemplates(`
    Red Blue
    `); class App extends Widget {} const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
    Red
    "); bus.trigger("some-event"); await nextTick(); expect(fixture.innerHTML).toBe("
    Blue
    "); }); }); describe("component error handling (catchError)", () => { /** * This test suite requires often to wait for 3 ticks. Here is why: * - First tick is to let the app render and crash. * - When we crash, we call the catchError handler in a setTimeout (because we * need to wait for the previous rendering to be completely stopped). So, we * need to wait for the second tick. * - Then, when the handler changes the state, we need to wait for the interface * to be rerendered. * */ test("can catch an error in a component render function", async () => { const consoleError = console.error; console.error = jest.fn(); env.qweb.addTemplates(`
    Error handled
    hey
    `); const handler = jest.fn(); env.qweb.on("error", null, handler); class ErrorComponent extends Widget {} class ErrorBoundary extends Widget { state = { error: false }; catchError() { this.state.error = true; } } class App extends Widget { state = { flag: false }; components = { ErrorBoundary, ErrorComponent }; } const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
    hey
    "); app.state.flag = true; await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
    Error handled
    "); expect(console.error).toBeCalledTimes(1); console.error = consoleError; expect(handler).toBeCalledTimes(1); }); test("no component catching error lead to full app destruction", async () => { const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); env.qweb.addTemplates(`
    hey
    `); class ErrorComponent extends Widget {} class App extends Widget { state = { flag: false }; components = { ErrorComponent }; } const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
    hey
    "); app.state.flag = true; await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe(""); expect(console.error).toBeCalledTimes(1); console.error = consoleError; expect(app.__owl__.isDestroyed).toBe(true); expect(handler).toBeCalledTimes(1); }); test("can catch an error in the initial call of a component render function", async () => { const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); env.qweb.addTemplates(`
    Error handled
    hey
    `); class ErrorComponent extends Widget {} class ErrorBoundary extends Widget { state = { error: false }; catchError() { this.state.error = true; } } class App extends Widget { components = { ErrorBoundary, ErrorComponent }; } const app = new App(env); await app.mount(fixture); await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
    Error handled
    "); expect(console.error).toBeCalledTimes(1); console.error = consoleError; expect(handler).toBeCalledTimes(1); }); test("can catch an error in the constructor call of a component render function", async () => { const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); env.qweb.addTemplates(`
    Error handled
    Some text
    `); class ErrorComponent extends Widget { constructor(parent) { super(parent); throw new Error("NOOOOO"); } } class ErrorBoundary extends Widget { state = { error: false }; catchError() { this.state.error = true; } } class App extends Widget { components = { ErrorBoundary, ErrorComponent }; } const app = new App(env); await app.mount(fixture); await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
    Error handled
    "); expect(console.error).toBeCalledTimes(1); console.error = consoleError; expect(handler).toBeCalledTimes(1); }); test("can catch an error in the willStart call", async () => { const consoleError = console.error; console.error = jest.fn(); env.qweb.addTemplates(`
    Error handled
    Some text
    `); class ErrorComponent extends Widget { async willStart() { // we wait a little bit to be in a different stack frame await nextTick(); throw new Error("NOOOOO"); } } class ErrorBoundary extends Widget { state = { error: false }; catchError() { this.state.error = true; } } class App extends Widget { components = { ErrorBoundary, ErrorComponent }; } const app = new App(env); await app.mount(fixture); await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
    Error handled
    "); expect(console.error).toBeCalledTimes(1); console.error = consoleError; }); test("can catch an error in the mounted call", async () => { console.error = jest.fn(); env.qweb.addTemplates(`
    Error handled
    Some text
    `); class ErrorComponent extends Widget { mounted() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Widget { state = { error: false }; catchError() { this.state.error = true; } } class App extends Widget { components = { ErrorBoundary, ErrorComponent }; } const app = new App(env); await app.mount(fixture); await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
    Error handled
    "); }); test("can catch an error in the willPatch call", async () => { env.qweb.addTemplates(`
    Error handled
    `); class ErrorComponent extends Widget { willPatch() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Widget { state = { error: false }; catchError() { this.state.error = true; } } class App extends Widget { state = { message: "abc" }; components = { ErrorBoundary, ErrorComponent }; } const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
    abc
    "); app.state.message = "def"; await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
    Error handled
    "); }); }); describe("top level sub widgets", () => { test("basic use", async () => { env.qweb.addTemplates(` child `); class Child extends Widget {} class Parent extends Widget { components = { Child }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("child1"); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); }); test("sub widget is interactive", async () => { env.qweb.addTemplates(` child `); class Child extends Widget { state = { val: 1 }; inc() { this.state.val++; } } class Parent extends Widget { components = { Child }; } const parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("child1"); const button = fixture.querySelector("button")!; button.click(); await nextTick(); expect(fixture.innerHTML).toBe("child2"); }); test("can select a sub widget ", async () => { env.qweb.addTemplates(` CHILD 1
    CHILD 2
    `); class Child extends Widget {} class OtherChild extends Widget {} class Parent extends Widget { components = { Child, OtherChild }; } (env).flag = true; let parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("CHILD 1"); parent.destroy(); (env).flag = false; parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    CHILD 2
    "); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); }); test("can select a sub widget, part 2", async () => { env.qweb.addTemplates(` CHILD 1
    CHILD 2
    `); class Child extends Widget {} class OtherChild extends Widget {} class Parent extends Widget { state = { flag: true }; components = { Child, OtherChild }; } let parent = new Parent(env); await parent.mount(fixture); expect(fixture.innerHTML).toBe("CHILD 1"); parent.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
    CHILD 2
    "); }); }); describe("unmounting and remounting", () => { test("widget can be unmounted and remounted", async () => { env.qweb.addTemplates(`
    Hey
    `); const steps: string[] = []; class MyWidget extends Widget { async willStart() { steps.push("willstart"); } mounted() { steps.push("mounted"); } willUnmount() { steps.push("willunmount"); } } const w = new MyWidget(env); await w.mount(fixture); expect(fixture.innerHTML).toBe("
    Hey
    "); expect(steps).toEqual(["willstart", "mounted"]); w.unmount(); expect(fixture.innerHTML).toBe(""); expect(steps).toEqual(["willstart", "mounted", "willunmount"]); await w.mount(fixture); expect(fixture.innerHTML).toBe("
    Hey
    "); expect(steps).toEqual(["willstart", "mounted", "willunmount", "mounted"]); }); test("widget can be mounted twice without ill effect", async () => { env.qweb.addTemplates(`
    Hey
    `); const steps: string[] = []; class MyWidget extends Widget { async willStart() { steps.push("willstart"); } mounted() { steps.push("mounted"); } willUnmount() { steps.push("willunmount"); } } const w = new MyWidget(env); await w.mount(fixture); await w.mount(fixture); expect(fixture.innerHTML).toBe("
    Hey
    "); expect(steps).toEqual(["willstart", "mounted"]); }); test("state changes in willUnmount do not trigger rerender", async () => { const steps: string[] = []; env.qweb.addTemplates(`
    `); class Child extends Widget { state = { n: 2 }; __render(a, b, c, d) { steps.push("render"); return super.__render(a, b, c, d); } willPatch() { steps.push("willPatch"); } patched() { steps.push("patched"); } willUnmount() { steps.push("willUnmount"); this.state.n = 3; } } class Parent extends Widget { components = { Child }; state = { val: 1, flag: true }; } const widget = new Parent(env); await widget.mount(fixture); expect(steps).toEqual(["render"]); expect(fixture.innerHTML).toBe("
    12
    "); widget.state.flag = false; await nextTick(); // we make sure here that no call to __render is done expect(steps).toEqual(["render", "willUnmount"]); }); test("state changes in willUnmount will be applied on remount", async () => { env.qweb.addTemplates(`
    `); class TestWidget extends Widget { state = { val: 1 }; willUnmount() { this.state.val = 3; } } const widget = new TestWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
    1
    "); widget.unmount(); expect(fixture.innerHTML).toBe(""); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
    1
    "); widget.unmount(); await widget.mount(fixture, true); expect(fixture.innerHTML).toBe("
    3
    "); }); });