import { Component, Env } from "../../src/component/component"; import { QWeb } from "../../src/qweb/qweb"; import { xml } from "../../src/tags"; import { useState, useRef } from "../../src/hooks"; 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("WidgetA", `
Hello
`); env.qweb.addTemplate("WidgetB", `
world
`); Component.env = env; }); afterEach(() => { fixture.remove(); }); function children(w: Component): Component[] { const childrenMap = w.__owl__.children; return Object.keys(childrenMap).map(id => childrenMap[id]); } // Test components class WidgetB extends Component {} class WidgetA extends Component { static components = { b: WidgetB }; } //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ describe("basic widget properties", () => { test("props is set on root components", async () => { const widget = new Component(null, {}); expect(widget.props).toEqual({}); }); test("has no el after creation", async () => { const widget = new Component(); expect(widget.el).toBe(null); }); test("can be mounted", async () => { class SomeWidget extends Component { static template = xml`
content
`; } const widget = new SomeWidget(); widget.mount(fixture); await nextTick(); expect(fixture.innerHTML).toBe("
content
"); }); test("can be mounted on a documentFragment", async () => { class SomeWidget extends Component { static template = xml`
content
`; } const widget = new SomeWidget(); await widget.mount(document.createDocumentFragment()); expect(fixture.innerHTML).toBe(""); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
content
"); }); test("display a nice message if mounted on a non existing node", async () => { class SomeWidget extends Component { static template = xml`
content
`; } const widget = new SomeWidget(); let error; try { await widget.mount(null as any); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe( "Component 'SomeWidget' cannot be mounted: the target is not a valid DOM node.\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)" ); }); test("display an error message if result of rendering is empty", async () => { class SomeWidget extends Component { static template = xml``; } const widget = new SomeWidget(); let error; try { await widget.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Rendering 'SomeWidget' did not return anything"); }); test("crashes if it cannot find a template", async () => { expect.assertions(1); class SomeWidget extends Component {} try { new SomeWidget(); } catch (e) { expect(e.message).toBe('Could not find template for component "SomeWidget"'); } }); test("can be clicked on and updated", async () => { class Counter extends Component { static template = xml`
`; state = useState({ counter: 0 }); } const counter = new Counter(); counter.mount(fixture); await nextTick(); expect(fixture.innerHTML).toBe("
0
"); const button = (counter.el).getElementsByTagName("button")[0]; button.click(); await nextTick(); expect(fixture.innerHTML).toBe("
1
"); }); test("can handle empty props", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; } const parent = new Parent(); await parent.mount(fixture); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); expect(fixture.innerHTML).toBe( "
"); }); test("cannot be clicked on and updated if not in DOM", async () => { class Counter extends Component { static template = xml`
`; state = useState({ counter: 0 }); } const counter = new Counter(); const target = document.createElement("div"); await counter.mount(target); expect(target.innerHTML).toBe("
0
"); const button = (counter.el).getElementsByTagName("button")[0]; button.click(); await nextTick(); expect(target.innerHTML).toBe("
0
"); expect(counter.state.counter).toBe(0); }); test("widget style and classname", async () => { class StyledWidget extends Component { static template = xml`
world
`; } const widget = new StyledWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
world
`); }); test("changing state before first render does not trigger a render", async () => { const steps: string[] = []; class TestW extends Component { static template = xml`
`; state = useState({ drinks: 1 }); async willStart() { this.state.drinks++; } async __render(f) { steps.push("__render"); return super.__render(f); } mounted() { steps.push("mounted"); } patched() { steps.push("patched"); } } const widget = new TestW(); await widget.mount(fixture); expect(steps).toEqual(["__render", "mounted"]); }); test("changing state before first render does not trigger a render (with parent)", async () => { const steps: string[] = []; class TestW extends Component { static template = xml`W`; state = useState({ drinks: 1 }); async willStart() { this.state.drinks++; } async __render(f) { steps.push("__render"); return super.__render(f); } mounted() { steps.push("mounted"); } patched() { steps.push("patched"); } } class Parent extends Component { state = useState({ flag: false }); static components = { TestW }; static template = xml`
`; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
"); expect(steps).toEqual([]); parent.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
W
"); expect(steps).toEqual(["__render", "mounted"]); }); test("render method wait until rendering is done", async () => { class TestW extends Component { static template = xml`
`; state = { drinks: 1 }; } const widget = new TestW(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
1
"); widget.state.drinks = 2; const renderPromise = widget.render(); expect(fixture.innerHTML).toBe("
1
"); await renderPromise; expect(fixture.innerHTML).toBe("
2
"); }); test("keeps a reference to env", async () => { const widget = new Component(); expect(widget.env).toBe(env); }); test("do not remove previously rendered dom if not necessary", async () => { class SomeComponent extends Component { static template = xml`
`; } const widget = new SomeComponent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); widget.el!.appendChild(document.createElement("span")); expect(fixture.innerHTML).toBe(`
`); widget.render(); // FIXME? expect(fixture.innerHTML).toBe(`
`); }); test("reconciliation alg is not confused in some specific situation", async () => { // in this test, we set t-key to 4 because it was in conflict with the // template id corresponding to the first child. class Child extends Component { static template = xml`child`; } class Parent extends Component { static template = xml`
`; static components = { Child }; } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
childchild
"); }); test("reconciliation alg works for t-foreach in t-foreach", async () => { const warn = console.warn; console.warn = () => {}; class Child extends Component { static template = xml`
`; } class Parent extends Component { static template = xml`
`; static components = { Child }; state = { s: [{ blips: ["a1", "a2"] }, { blips: ["b1"] }] }; } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
a1
a2
b1
"); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); console.warn = warn; }); test("reconciliation alg works for t-foreach in t-foreach, 2", async () => { class Child extends Component { static template = xml`
`; } class Parent extends Component { static template = xml`

`; static components = { Child }; state = useState({ rows: [1, 2], cols: ["a", "b"] }); } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe( "

1_a

1_b

2_a

2_b

" ); widget.state.rows = [2, 1]; await nextTick(); expect(fixture.innerHTML).toBe( "

2_a

2_b

1_a

1_b

" ); }); test("same t-keys in two different places", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
1
2
"); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); }); test("t-key on a component with t-if, and a sibling component", async () => { class Child extends Component { static template = xml`child`; } class Parent extends Component { static template = xml`
`; static components = { Child }; } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
child
"); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); }); }); describe("mount targets", () => { test("can attach a component to an existing node (if same tagname)", async () => { class App extends Component { static template = xml`
app
`; } const div = document.createElement("div"); fixture.appendChild(div); const app = new App(); await app.mount(div, { position: "self" }); expect(fixture.innerHTML).toBe("
app
"); }); test("cannot attach a component to an existing node (if not same tagname)", async () => { class App extends Component { static template = xml`app`; } const div = document.createElement("div"); fixture.appendChild(div); const app = new App(); let error; try { await app.mount(div, { position: "self" }); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)"); }); test("can mount a component (with position='first-child')", async () => { class App extends Component { static template = xml`
app
`; } const span = document.createElement("span"); fixture.appendChild(span); const app = new App(); await app.mount(fixture, { position: "first-child" }); expect(fixture.innerHTML).toBe("
app
"); }); test("can mount a component (with position='last-child')", async () => { class App extends Component { static template = xml`
app
`; } const span = document.createElement("span"); fixture.appendChild(span); const app = new App(); await app.mount(fixture, { position: "last-child" }); expect(fixture.innerHTML).toBe("
app
"); }); test("default mount option is 'last-child'", async () => { class App extends Component { static template = xml`
app
`; } const span = document.createElement("span"); fixture.appendChild(span); const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
app
"); }); }); describe("lifecycle hooks", () => { test("willStart hook is called", async () => { let willstart = false; class HookWidget extends Component { static template = xml`
`; async willStart() { willstart = true; } } const widget = new HookWidget(); 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 Component { static template = xml`
`; async mounted() { mounted = true; } } const widget = new HookWidget(); 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 Component { static template = xml`
`; async mounted() { mounted = true; } } const widget = new HookWidget(); await widget.mount(fixture); expect(mounted).toBe(true); }); test("willStart hook is called on subwidget", async () => { let ok = false; class ChildWidget extends Component { static template = xml`
`; async willStart() { ok = true; } } class ParentWidget extends Component { static template = xml`
`; static components = { child: ChildWidget }; } const widget = new ParentWidget(); await widget.mount(fixture); expect(ok).toBe(true); }); test("mounted hook is called on subcomponents, in proper order", async () => { const steps: any[] = []; class ChildWidget extends Component { static template = xml`
`; mounted() { expect(document.body.contains(this.el)).toBe(true); steps.push("child:mounted"); } } class ParentWidget extends Component { static template = xml`
`; static components = { ChildWidget }; mounted() { steps.push("parent:mounted"); } } const widget = new ParentWidget(); 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[] = []; class ChildChildWidget extends Component { static template = xml`
`; mounted() { steps.push("childchild:mounted"); } willUnmount() { steps.push("childchild:willUnmount"); } } class ChildWidget extends Component { static template = xml`
`; static components = { childchild: ChildChildWidget }; mounted() { steps.push("child:mounted"); } willUnmount() { steps.push("child:willUnmount"); } } class ParentWidget extends Component { static template = xml`
`; static components = { child: ChildWidget }; state = useState({ flag: false }); mounted() { steps.push("parent:mounted"); } willUnmount() { steps.push("parent:willUnmount"); } } const widget = new ParentWidget(); 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("ChildWidget", `
`); env.qweb.addTemplate("ChildChildWidget", `
`); class ChildChildWidget extends Component { willPatch() { steps.push("childchild:willPatch"); } patched() { steps.push("childchild:patched"); } } class ChildWidget extends Component { static components = { childchild: ChildChildWidget }; willPatch() { steps.push("child:willPatch"); } patched() { steps.push("child:patched"); } } env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Component { static components = { child: ChildWidget }; state = useState({ n: 1 }); willPatch() { steps.push("parent:willPatch"); } patched() { steps.push("parent:patched"); } } const widget = new ParentWidget(); 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 ChildWidget extends Component { static template = xml`
`; async willStart() { steps.push("child:willStart"); } mounted() { steps.push("child:mounted"); } } class ParentWidget extends Component { state = useState({ ok: false }); static components = { child: ChildWidget }; } const widget = new ParentWidget(); 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 () => { // 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); class ParentWidget extends Component { static template = xml`
`; mounted() { const child = new ChildWidget(this); child.mount(this.el!); } } class ChildWidget extends Component { static template = xml`
`; mounted() { expect(this.el).toBeTruthy(); } } const widget = new ParentWidget(); await widget.mount(fixture); // wait for ParentWidget await nextTick(); // wait for ChildWidget }); test("components are unmounted and destroyed if no longer in DOM", async () => { let steps: string[] = []; class ChildWidget extends Component { static template = xml`
`; constructor(parent) { super(parent); steps.push("init"); } async willStart() { steps.push("willstart"); } mounted() { steps.push("mounted"); } willUnmount() { steps.push("willunmount"); } } class ParentWidget extends Component { static template = xml`
`; static components = { ChildWidget }; state = useState({ ok: true }); } const widget = new ParentWidget(); 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; class ChildWidget extends Component { static template = xml``; willUnmount() { childUnmounted = true; } } class ParentWidget extends Component { static template = xml`
`; static components = { ChildWidget }; state = useState({ n: 0, flag: true }); increment() { this.state.n += 1; } toggleSubWidget() { this.state.flag = !this.state.flag; } } const widget = new ParentWidget(); 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 ChildWidget extends Component { static template = xml`
`; constructor(parent) { super(parent); steps.push("c init"); } async willStart() { steps.push("c willstart"); } mounted() { steps.push("c mounted"); } willUnmount() { steps.push("c willunmount"); } } class ParentWidget extends Component { static template = xml`
`; static 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"); } } const widget = new ParentWidget(); 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 HookWidget extends Component { static template = xml``; willUpdateProps(nextProps) { expect(nextProps.n).toBe(2); return def; } } class Parent extends Component { state = useState({ n: 1 }); static components = { Child: HookWidget }; } const widget = new Parent(); 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 Component { static template = xml`
`; state = useState({ a: 1 }); patched() { n++; } } const widget = new TestWidget(); 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 TestWidget extends Component { static template = xml`
`; patched() { n++; } } class Parent extends Component { static template = xml`
`; state = useState({ a: 1 }); static components = { Child: TestWidget }; } const widget = new Parent(); await widget.mount(fixture); expect(n).toBe(0); widget.state.a = 2; await nextTick(); expect(n).toBe(1); }); test("shouldUpdate hook prevent rerendering", async () => { let shouldUpdate = false; class TestWidget extends Component { static template = xml`
`; shouldUpdate() { return shouldUpdate; } } class Parent extends Component { static template = xml`
`; static components = { Child: TestWidget }; state = useState({ val: 42 }); } const widget = new Parent(); 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 ChildWidget extends Component { static template = xml`
`; constructor(parent, props) { super(parent, props); created = true; } mounted() { mounted = true; } } class ParentWidget extends Component { static template = xml`
`; static components = { child: ChildWidget }; state = useState({ flag: false }); } const widget = new ParentWidget(); 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 ChildWidget extends Component { static template = xml`
`; willPatch() { steps.push("child:willPatch"); } patched() { steps.push("child:patched"); } } class ParentWidget extends Component { static template = xml`
`; static components = { child: ChildWidget }; state = useState({ n: 1 }); willPatch() { steps.push("parent:willPatch"); } patched() { steps.push("parent:patched"); } } const widget = new ParentWidget(); 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" ]); }); }); describe("destroy method", () => { test("destroy remove the widget from the DOM", async () => { class SomeComponent extends Component { static template = xml`
`; } const widget = new SomeComponent(); 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(); 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(); 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 Component { static template = xml`
`; willStart() { return def; } } expect(fixture.innerHTML).toBe(""); const widget = new DelayedWidget(); 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); }); test("destroying a widget before being mounted", async () => { class GrandChild extends Component { static template = xml` `; } class Child extends Component { static template = xml` `; static components = { GrandChild }; state = useState({ val: 33, flag: false }); doSomething() { this.state.val = 12; this.state.flag = true; this.trigger("some-event"); } get something() { return { val: this.state.val }; } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ p: 1 }); doStuff() { this.state.p = 2; } } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
"); fixture.querySelector("button")!.click(); await nextTick(); expect(fixture.innerHTML).toBe("
12
"); }); test("destroying a widget before being mounted (2)", async () => { const steps: string[] = []; class Child extends Component { static template = xml``; willStart() { steps.push("willStart"); return makeDeferred(); } __destroy(parent) { steps.push("__destroy"); super.__destroy(parent); } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ flag: true }); } const parent = new Parent(); const prom = parent.mount(fixture); await nextTick(); // wait for Child to be instantiated parent.state.flag = false; await prom; expect(fixture.innerHTML).toBe("
"); expect(steps).toEqual(["willStart", "__destroy"]); }); }); describe("composition", () => { test("a widget with a sub widget", async () => { const widget = new WidgetA(); 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.registerComponent("WidgetB", WidgetB); env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Component {} const widget = new ParentWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
world
"); delete QWeb.components["WidgetB"]; }); test("can use dynamic components (the class) if given", async () => { class A extends Component { static template = xml`child a`; } class B extends Component { static template = xml`child b`; } class App extends Component { static template = xml``; state = useState({ child: "a" }); get myComponent() { return this.state.child === "a" ? A : B; } } const widget = new App(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("child a"); widget.state.child = "b"; await nextTick(); expect(fixture.innerHTML).toBe("child b"); }); test("can use dynamic components (the class) if given (with different root tagname)", async () => { class A extends Component { static template = xml`child a`; } class B extends Component { static template = xml`
child b
`; } class App extends Component { static template = xml``; state = useState({ child: "a" }); get myComponent() { return this.state.child === "a" ? A : B; } } const widget = new App(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("child a"); widget.state.child = "b"; await nextTick(); expect(fixture.innerHTML).toBe("
child b
"); }); test("don't fallback to global registry if widget defined locally", async () => { QWeb.registerComponent("WidgetB", WidgetB); // should not use this widget env.qweb.addTemplate("ParentWidget", `
`); env.qweb.addTemplate("AnotherWidgetB", `Belgium`); class AnotherWidgetB extends Component {} class ParentWidget extends Component { static components = { WidgetB: AnotherWidgetB }; } const widget = new ParentWidget(); 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 Component {} class P extends Component { static components = { C }; } const parent = new P(); 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(); class SomeComponent extends Component {} env.qweb.addTemplate("Parent", `
`); class Parent extends Component { static components = { SomeWidget: SomeComponent }; } const parent = new Parent(); let error; try { await parent.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe('Cannot find the definition of component "SomeMispelledWidget"'); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("modifying a sub widget", async () => { class Counter extends Component { static template = xml`
`; state = useState({ counter: 0 }); } env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Component { static components = { Counter }; } const widget = new ParentWidget(); 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 SomeComponent extends Component { static template = xml`
`; } class ParentWidget extends Component { static components = { Child: SomeComponent }; elem1 = useRef("1"); elem2 = useRef("2"); elem3 = useRef("3"); elem4 = useRef("4"); state = useState({ items: [1, 2, 3] }); } const parent = new ParentWidget(); await parent.mount(fixture); expect(parent.elem1.comp).toBeDefined(); expect(parent.elem2.comp).toBeDefined(); expect(parent.elem3.comp).toBeDefined(); expect(parent.elem4.comp).toBeNull(); }); test("parent's elm for a children === children's elm, even after rerender", async () => { const widget = new WidgetA(); 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(); await widget.mount(fixture); expect(children(widget)[0].env).toBe(env); }); test("rerendering a widget with a sub widget", async () => { class Counter extends Component { static template = xml`
`; state = useState({ counter: 0 }); } env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Component { static components = { Counter }; } const widget = new ParentWidget(); 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 () => { class Counter extends Component { static template = xml`
`; state = useState({ counter: 0 }); } env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Component { state = useState({ ok: true }); static components = { Counter }; } const widget = new ParentWidget(); 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 rendered in a loop", async () => { env.qweb.addTemplate("ChildWidget", ``); class ChildWidget extends Component {} env.qweb.addTemplate( "Parent", `
` ); class Parent extends Component { state = useState({ numbers: [1, 2, 3] }); static components = { ChildWidget }; } const parent = new Parent(); 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 Component { state: any; constructor(parent) { super(parent); this.state = useState({ n }); n++; } } env.qweb.addTemplate( "parent", `
` ); class Parent extends Component { static template = "parent"; state = useState({ numbers: [1, 2, 3] }); static components = { ChildWidget }; } const parent = new Parent(); 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... class ChildWidget extends Component { static template = xml`child`; } class Parent extends Component { static template = xml`

hey

noo

test
`; static components = { ChildWidget }; state = useState({ flag: false }); } const parent = new Parent(); await parent.mount(fixture); const child = children(parent)[0]; expect(normalize(fixture.innerHTML)).toBe( normalize(`

noo

child
`) ); 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("list of sub components inside other nodes", async () => { // this confuses the patching algorithm... env.qweb.addTemplate("ChildWidget", `child`); env.qweb.addTemplates(`
asdf
`); class SubWidget extends Component {} class Parent extends Component { static components = { SubWidget }; state = useState({ blips: [ { a: "a", id: 1 }, { b: "b", id: 2 }, { c: "c", id: 4 } ] }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe( "
asdf
asdf
asdf
" ); parent.state.blips.splice(0, 1); await nextTick(); expect(fixture.innerHTML).toBe( "
asdf
asdf
" ); }); test("list of two sub components inside other nodes", async () => { // this confuses the patching algorithm... env.qweb.addTemplate("ChildWidget", `child`); env.qweb.addTemplates(`
asdf
`); class SubWidget extends Component {} class Parent extends Component { static components = { SubWidget }; state = useState({ blips: [{ a: "a", id: 1 }] }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
asdfasdf
"); }); test("t-component with dynamic value", async () => { env.qweb.addTemplate("ParentWidget", `
`); class ParentWidget extends Component { static components = { WidgetB }; state = useState({ widget: "WidgetB" }); } const widget = new ParentWidget(); 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 Component { static components = { WidgetB }; state = useState({ widget: "B" }); } const widget = new ParentWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
world
"); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); }); test("t-component not on a node", async () => { class Child extends Component { static template = xml`1`; } class Parent extends Component { static components = { Child }; static template = xml`
`; } const parent = new Parent(); let error; try { await parent.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe( `Directive 't-component' can only be used on nodes (used on a
)` ); }); test("sub components, loops, and shouldUpdate", async () => { class ChildWidget extends Component { static template = xml``; shouldUpdate(nextProps) { if (nextProps.val === 12) { return false; } return true; } } class Parent extends Component { static template = xml`
`; state = useState({ records: [ { id: 1, val: 1 }, { id: 2, val: 2 }, { id: 3, val: 3 } ] }); static components = { ChildWidget }; } const parent = new Parent(); await parent.mount(fixture); expect(normalize(fixture.innerHTML)).toBe( "
123
" ); parent.state.records[0].val = 11; parent.state.records[1].val = 12; parent.state.records[2].val = 13; await nextTick(); expect(normalize(fixture.innerHTML)).toBe( "
11213
" ); }); test("three level of components with collapsing root nodes", async () => { class GrandChild extends Component { static template = xml`
2
`; } class Child extends Component { static components = { GrandChild }; static template = xml``; } class Parent extends Component { static components = { Child }; static template = xml``; } const app = new Parent(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
2
"); }); }); describe("props evaluation ", () => { test("explicit object prop", async () => { env.qweb.addTemplate("Parent", `
`); class Child extends Component { state: { someval: number }; constructor(parent: Parent, props: { value: number }) { super(parent); this.state = useState({ someval: props.value }); } } class Parent extends Component { static components = { child: Child }; state = useState({ val: 42 }); } env.qweb.addTemplate("Child", ``); const widget = new Parent(); 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 Component {} env.qweb.addTemplate("Parent", `
`); class Parent extends Component { static components = { child: Child }; get greetings() { const name = "aaron"; return `hello ${name}`; } } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
hello aaron
"); }); test("t-set works ", async () => { class Child extends Component {} env.qweb.addTemplate( "Parent", `
` ); class Parent extends Component { static components = { child: Child }; } env.qweb.addTemplate( "Child", ` ` ); const widget = new Parent(); 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 () => { class Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static template = xml`
`; static components = { Child }; } const widget = new ParentWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); }); test("empty class attribute is not added on widget root el", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); }); test("t-att-class is properly added/removed on widget root el", async () => { class Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static template = xml`
`; static components = { Child }; state = useState({ a: true, b: false }); } const widget = new ParentWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); expect(QWeb.TEMPLATES[ParentWidget.template].fn.toString()); 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 Child extends Component {} class ParentWidget extends Component { static components = { Child }; } env.qweb.addTemplate("Child", `
`); const widget = new ParentWidget(); 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 Child extends Component { state = useState({ d: true }); } class ParentWidget extends Component { static components = { Child }; state = useState({ b: true }); child = useRef("child"); } const widget = new ParentWidget(); 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.child.comp as 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.child.comp as 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 Child extends Component { state = useState({ d: true }); } class ParentWidget extends Component { static components = { Child }; state = useState({ b: true }); child = useRef("child"); } const widget = new ParentWidget(); 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.child.comp as 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.child.comp as 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 Component { state = useState({ c: true }); mounted() { this.el!.classList.add("user"); } } const widget = new App(); 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 SomeComponent extends Component { static template = xml`
`; } class ParentWidget extends Component { static components = { child: SomeComponent }; } const widget = new ParentWidget(); 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 SomeComponent extends Component { static template = xml`
`; } class ParentWidget extends Component { static components = { child: SomeComponent }; state = useState({ style: "font-size: 20px" }); } const widget = new ParentWidget(); 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(`
`); }); test("error in subcomponent with class", async () => { class Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static template = xml`
`; static components = { Child }; } const widget = new ParentWidget(); let error; try { await widget.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Cannot read property 'crash' of undefined"); expect(fixture.innerHTML).toBe(""); }); }); describe("other directives with t-component", () => { test("t-on works as expected", async () => { expect.assertions(4); env.qweb.addTemplates(`
`); class Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static components = { child: Child }; n = 0; someMethod(ev) { expect(ev.detail).toBe(43); this.n++; } } const widget = new ParentWidget(); 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 works, even if flush is called many times", async () => { let flag = false; let mounted = false; class Child extends Component { static template = xml`child`; mounted() { mounted = true; } } class Parent extends Component { static template = xml`
`; static components = { Child }; doSomething() { flag = true; } } const parent = new Parent(); parent.mount(fixture); while (!mounted) { await nextMicroTick(); Component.scheduler.flush(); } expect(fixture.innerHTML).toBe("
child
"); expect(flag).toBe(false); fixture.querySelector("span")!.click(); expect(flag).toBe(true); }); test("t-on with inline statement", async () => { class Child extends Component { static template = xml`child`; } class Parent extends Component { static template = xml`
`; static components = { Child }; state = { n: 3 }; } const parent = new Parent(); await parent.mount(fixture); expect(parent.state.n).toBe(3); fixture.querySelector("span")!.click(); expect(parent.state.n).toBe(4); }); test("t-on with handler bound to argument", async () => { expect.assertions(3); env.qweb.addTemplates(`
`); class Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static components = { child: Child }; onEv(n, ev) { expect(n).toBe(3); expect(ev.detail).toBe(43); } } const widget = new ParentWidget(); 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 Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static components = { child: Child }; onEv(o, ev) { expect(o).toEqual({ val: 3 }); expect(ev.detail).toBe(43); } } const widget = new ParentWidget(); 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 Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static components = { child: Child }; onEv(o, ev) { expect(o).toEqual({}); expect(ev.detail).toBe(43); } } const widget = new ParentWidget(); 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 Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static components = { child: Child }; onEv(o, ev) { expect(o).toEqual({}); expect(ev.detail).toBe(43); } } const widget = new ParentWidget(); 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 Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static 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); } } const widget = new ParentWidget(); 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 GrandChild extends Component { static template = xml`
`; } class Child extends Component { static components = { child: GrandChild }; } class ParentWidget extends Component { static components = { child: Child }; onEv1(ev) { steps.push("onEv1"); } onEv2(ev) { steps.push("onEv2"); } } const widget = new ParentWidget(); 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 GrandChild extends Component { static template = xml`
`; } class Child extends Component { static components = { child: GrandChild }; } class ParentWidget extends Component { static components = { child: Child }; onEv() {} } const widget = new ParentWidget(); 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 GrandChild extends Component { static template = xml`
`; } class Child extends Component { static components = { GrandChild }; } class ParentWidget extends Component { static components = { Child }; onEv() {} } const widget = new ParentWidget(); 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-on with getter as handler", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ counter: 0 }); get handler() { this.state.counter++; return () => {}; } } const parent = new Parent(); await parent.mount(fixture); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); expect(fixture.innerHTML).toBe("
0
"); let child = children(parent)[0]; child.trigger("ev"); await nextTick(); expect(fixture.innerHTML).toBe("
1
"); }); test("t-on with inline statement", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ counter: 0 }); } const parent = new Parent(); await parent.mount(fixture); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); expect(fixture.innerHTML).toBe("
0
"); let child = children(parent)[0]; child.trigger("ev"); await nextTick(); expect(fixture.innerHTML).toBe("
1
"); }); test("t-on with no handler (only modifiers)", async () => { expect.assertions(2); class ComponentB extends Component { static template = xml``; } class ComponentA extends Component { static template = xml`

`; static components = { ComponentB }; } class Parent extends Component { static template = xml`
`; static components = { ComponentA }; onEv(ev) { expect(ev.defaultPrevented).toBe(true); } } const parent = new Parent(); await parent.mount(fixture); let componentB = children(children(parent)[0])[0]; componentB.trigger("ev"); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); }); test("t-on on nested components with collapsing root nodes", async () => { const steps: string[] = []; let grandChild; class GrandChild extends Component { static template = xml``; constructor() { super(...arguments); grandChild = this; } _onEv() { steps.push("GrandChild"); } } class Child extends Component { static template = xml``; static components = { GrandChild }; _onEv() { steps.push("Child"); } } class Parent extends Component { static template = xml``; static components = { Child }; _onEv() { steps.push("Parent"); } } const parent = new Parent(); await parent.mount(fixture); grandChild.trigger("ev"); expect(steps).toEqual(["GrandChild", "Child", "Parent"]); }); test("t-on on unmounted components", async () => { const steps: string[] = []; let child; class Child extends Component { static template = xml`
`; mounted() { child = this; } onClick() { steps.push("click"); } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ flag: true }); } const parent = new Parent(); await parent.mount(fixture); let el = child.el; el.click(); expect(steps).toEqual(["click"]); parent.unmount(); expect(child.__owl__.isMounted).toBe(false); el.click(); expect(steps).toEqual(["click"]); }); test("t-on on destroyed components", async () => { const steps: string[] = []; let child; class Child extends Component { static template = xml`
`; mounted() { child = this; } onClick() { steps.push("click"); } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ flag: true }); } const parent = new Parent(); await parent.mount(fixture); let el = child.el; el.click(); expect(steps).toEqual(["click"]); parent.state.flag = false; await nextTick(); expect(child.__owl__.isDestroyed).toBe(true); el.click(); expect(steps).toEqual(["click"]); }); test("t-on on destroyed components, part 2", async () => { const steps: string[] = []; let child; class GrandChild extends Component { static template = xml`
grandchild
`; } class Child extends Component { static template = xml`
`; static components = { GrandChild }; gc = useRef("gc"); mounted() { child = this; } onClick() { steps.push("click"); } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ flag: true }); } const parent = new Parent(); await parent.mount(fixture); let el = child.gc.el; el.click(); expect(steps).toEqual(["click"]); parent.state.flag = false; await nextTick(); expect(child.__owl__.isDestroyed).toBe(true); el.click(); expect(steps).toEqual(["click"]); }); test("t-if works with t-component", async () => { env.qweb.addTemplate("ParentWidget", `
`); class Child extends Component {} class ParentWidget extends Component { static components = { child: Child }; state = useState({ flag: true }); } env.qweb.addTemplate("Child", "hey"); const widget = new ParentWidget(); 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 Child extends Component {} class ParentWidget extends Component { static components = { child: Child }; state = useState({ flag: true }); } env.qweb.addTemplate("Child", "hey"); const widget = new ParentWidget(); 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 Child extends Component {} class ParentWidget extends Component { static components = { child: Child }; state = useState({ flag: true }); } env.qweb.addTemplate("Child", "hey"); const widget = new ParentWidget(); 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 Child extends Component {} class ParentWidget extends Component { static components = { child: Child }; state = useState({ flag: true }); } env.qweb.addTemplate("Child", "hey"); const widget = new ParentWidget(); await widget.mount(fixture); expect(normalize(fixture.innerHTML)).toBe("
somediv
"); widget.state.flag = false; await nextTick(); expect(normalize(fixture.innerHTML)).toBe("
hey
"); }); test("t-foreach with t-component, and update", async () => { class Child extends Component { static template = xml` `; state = useState({ val: "A" }); mounted() { this.state.val = "B"; } } class ParentWidget extends Component { static components = { Child }; static template = xml`
`; } const widget = new ParentWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
A0A1
"); await nextTick(); // wait for changes triggered in mounted to be applied expect(fixture.innerHTML).toBe("
B0B1
"); }); }); 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 SomeComponent extends Component { static template = xml`
`; } class Test extends Component { static components = { widget: SomeComponent }; } const widget = new Test(); 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); class Child extends Component { static template = xml`
`; } class ParentWidget extends Component { static template = xml`
`; static components = { Child }; items = [1, 2, 3, 4]; onEv(n, ev) { expect(n).toBe(1); expect(ev.detail).toBe(43); } } const widget = new ParentWidget(); await widget.mount(fixture); children(widget)[0].trigger("ev", 43); expect(env.qweb.templates[ParentWidget.template].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 Child extends Component {} class Parent extends Component { static components = { child: Child }; state = useState({ flag: false }); } env.qweb.addTemplate("Child", `abcdef`); const widget = new Parent(); 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 Child extends Component {} class Parent extends Component { static components = { child: Child }; state = useState({ flag: false }); } env.qweb.addTemplate("Child", `abcdef`); const widget = new Parent(); 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 Component { name: string = "test"; async willStart() { steps.push(`${this.name}:willStart`); } __render(f) { steps.push(`${this.name}:render`); return super.__render(f); } __patch(vnode) { steps.push(`${this.name}:__patch`); super.__patch(vnode); } 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("B", `
B
`); class B extends TestWidget { name = "B"; constructor(parent, props) { super(parent, props); steps.push("B: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"); } } env.qweb.addTemplate( "C", `
C
` ); class C extends TestWidget { static components = { D, E, F }; name = "C"; state = useState({ flag: true }); constructor(parent, props) { super(parent, props); c = this; steps.push("C:constructor"); } } env.qweb.addTemplate("A", `
A
`); class A extends TestWidget { static components = { B, C }; name = "A"; } const a = new A(); 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", "E:__patch", "D:__patch", "C:__patch", "B:__patch", "A:__patch", "E:mounted", "D:mounted", "C:mounted", "B: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", "F:__patch", "D:__patch", "C:__patch", "E:willUnmount", "E:destroy", "F:mounted", "D:patched", "C:patched" ]); }); test("can inject values in tagged templates", async () => { const SUBTEMPLATE = xml``; class Parent extends Component { static template = xml`
`; state = useState({ n: 42 }); } const widget = new Parent(); await widget.mount(fixture); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); expect(fixture.innerHTML).toBe("
42
"); }); }); describe("async rendering", () => { test("destroying a widget before start is over", async () => { let def = makeDeferred(); class W extends Component { static template = xml`
`; willStart(): Promise { return def; } } const w = new W(); 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; class Child extends Component { static template = xml`child:`; constructor(parent, props) { super(parent, props); n++; } willStart(): Promise { return def; } } class W extends Component { static template = xml`
`; static components = { Child }; state = useState({ val: 1 }); } const w = new W(); 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(); let nbRenderings: number = 0; class ChildA extends Component { static template = xml``; willStart(): Promise { return defA; } getValue() { nbRenderings++; return "a"; } } class ChildB extends Component { static template = xml`b`; willStart(): Promise { return defB; } } class Parent extends Component { static template = xml`
`; static components = { ChildA, ChildB }; state = useState({ flagA: false, flagB: false }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
"); parent.state.flagA = true; await nextTick(); expect(fixture.innerHTML).toBe("
"); parent.state.flagB = true; await nextTick(); expect(fixture.innerHTML).toBe("
"); defB.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
"); expect(nbRenderings).toBe(0); defA.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
ab
"); expect(nbRenderings).toBe(1); }); test("creating two async components, scenario 2", async () => { let defA = makeDeferred(); let defB = makeDeferred(); class ChildA extends Component { static template = xml`a`; willUpdateProps(): Promise { return defA; } } class ChildB extends Component { static template = xml`b`; willStart(): Promise { return defB; } } class Parent extends Component { static template = xml`
`; static components = { ChildA, ChildB }; state = useState({ valA: 1, valB: 2, flagB: false }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
a1
"); parent.state.valA = 2; await nextTick(); expect(fixture.innerHTML).toBe("
a1
"); parent.state.flagB = true; await nextTick(); expect(fixture.innerHTML).toBe("
a1
"); defB.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
a1
"); defA.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
a2b2
"); }); test("creating two async components, scenario 3 (patching in the same frame)", async () => { let defA = makeDeferred(); let defB = makeDeferred(); class ChildA extends Component { static template = xml`a`; willUpdateProps(): Promise { return defA; } } class ChildB extends Component { static template = xml`b`; willStart(): Promise { return defB; } } class Parent extends Component { static template = xml`
`; static components = { ChildA, ChildB }; state = useState({ valA: 1, valB: 2, flagB: false }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
a1
"); parent.state.valA = 2; await nextTick(); expect(fixture.innerHTML).toBe("
a1
"); parent.state.flagB = true; await nextTick(); expect(fixture.innerHTML).toBe("
a1
"); defB.resolve(); expect(fixture.innerHTML).toBe("
a1
"); defA.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
a2b2
"); }); test("update a sub-component twice in the same frame", async () => { const steps: string[] = []; const defs = [makeDeferred(), makeDeferred()]; let index = 0; class ChildA extends Component { static template = xml``; willUpdateProps(): Promise { return defs[index++]; } patched() { steps.push("patched"); } } class Parent extends Component { static template = xml`
`; static components = { ChildA }; state = useState({ valA: 1 }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
1
"); parent.state.valA = 2; await nextTick(); expect(fixture.innerHTML).toBe("
1
"); parent.state.valA = 3; await nextTick(); expect(fixture.innerHTML).toBe("
1
"); defs[0].resolve(); await Promise.resolve(); defs[1].resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
3
"); expect(steps).toEqual(["patched"]); }); test("update a sub-component twice in the same frame, 2", async () => { const steps: string[] = []; class ChildA extends Component { static template = xml``; patched() { steps.push("patched"); } val() { steps.push("render"); return this.props.val; } } class Parent extends Component { static template = xml`
`; static components = { ChildA }; state = useState({ valA: 1 }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
1
"); parent.state.valA = 2; await nextMicroTick(); expect(steps).toEqual(["render"]); await nextMicroTick(); // For an unknown reason, this test fails on windows without the next microtick. It works // in linux and osx, but fails on at least this machine. // I do not see anything harmful in waiting an extra tick. But it is annoying to not // know what is different. await nextMicroTick(); expect(steps).toEqual(["render", "render"]); expect(fixture.innerHTML).toBe("
1
"); parent.state.valA = 3; await nextMicroTick(); expect(steps).toEqual(["render", "render"]); await nextMicroTick(); // same as above await nextMicroTick(); expect(steps).toEqual(["render", "render", "render"]); expect(fixture.innerHTML).toBe("
1
"); await nextTick(); expect(fixture.innerHTML).toBe("
3
"); expect(steps).toEqual(["render", "render", "render", "patched"]); }); test("components in a node in a t-foreach ", async () => { class Child extends Component {} class App extends Component { static components = { Child }; get items() { return [1, 2]; } } env.qweb.addTemplates(`
`); const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe( "
  • 1
  • 2
" ); }); test("properly behave when destroyed/unmounted while rendering ", async () => { const def = makeDeferred(); class SubChild extends Component { static template = xml`
`; willPatch() { throw new Error("Should not happen!"); } patched() { throw new Error("Should not happen!"); } willUpdateProps() { return def; } } class Child extends Component { static template = xml`
`; static components = { SubChild }; } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ flag: true, val: "Framboise Lindemans" }); } const parent = new Parent(); 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(); expect(fixture.innerHTML).toBe("
"); // with this, we remove child, and subchild, even though it is not finished // rendering from previous changes parent.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
"); // we now resolve def, so the child rendering is now complete. (def).resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
"); }); test.skip("reuse widget if possible, in some async situation", async () => { // this optimization has been temporarily deactivated env.qweb.addTemplates(` a b `); let destroyCount = 0; class ChildA extends Component { destroy() { destroyCount++; super.destroy(); } } class ChildB extends Component { willStart(): any { return new Promise(function() {}); } } class Parent extends Component { static components = { ChildA, ChildB }; state = useState({ valA: 1, valB: 2, flag: false }); } const parent = new Parent(); 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("rendering component again in next microtick", async () => { class Child extends Component { static template = xml`
Child
`; } class App extends Component { static template = xml`
`; static components = { Child }; async onClick() { (env as any).flag = true; this.render(); await Promise.resolve(); this.render(); } } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
"); fixture.querySelector("button")!.click(); await nextTick(); expect(fixture.innerHTML).toBe("
Child
"); }); test("concurrent renderings scenario 1", async () => { const def = makeDeferred(); let stateB; class ComponentC extends Component { static template = xml``; someValue() { return this.props.fromB; } willUpdateProps() { return def; } } ComponentC.prototype.someValue = jest.fn(ComponentC.prototype.someValue); class ComponentB extends Component { static components = { ComponentC }; static template = xml`

`; state = useState({ fromB: "b" }); constructor(parent, props) { super(parent, props); stateB = this.state; } } class ComponentA extends Component { static components = { ComponentB }; static template = xml`
`; state = useState({ fromA: 1 }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("

1b

"); stateB.fromB = "c"; await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); expect(ComponentC.prototype.someValue).toBeCalledTimes(1); def.resolve(); await nextTick(); expect(ComponentC.prototype.someValue).toBeCalledTimes(2); expect(fixture.innerHTML).toBe("

2c

"); }); test("concurrent renderings scenario 2", async () => { // this test asserts that a rendering initiated before another one, and that // ends after it, is re-mapped to that second rendering const defs = [makeDeferred(), makeDeferred()]; let index = 0; let stateB; class ComponentC extends Component { static template = xml``; willUpdateProps() { return defs[index++]; } } class ComponentB extends Component { static template = xml`

`; static components = { ComponentC }; state = useState({ fromB: "b" }); constructor(parent, props) { super(parent, props); stateB = this.state; } } class ComponentA extends Component { static template = xml`
`; static components = { ComponentB }; state = useState({ fromA: 1 }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("
1

1b

"); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("
1

1b

"); stateB.fromB = "c"; await nextTick(); expect(fixture.innerHTML).toBe("
1

1b

"); defs[1].resolve(); // resolve rendering initiated in B await nextTick(); expect(fixture.innerHTML).toBe("
2

2c

"); defs[0].resolve(); // resolve rendering initiated in A await nextTick(); expect(fixture.innerHTML).toBe("
2

2c

"); }); test("concurrent renderings scenario 2bis", async () => { const defs = [makeDeferred(), makeDeferred()]; let index = 0; let stateB; class ComponentC extends Component { static template = xml``; willUpdateProps() { return defs[index++]; } } class ComponentB extends Component { static template = xml`

`; static components = { ComponentC }; state = useState({ fromB: "b" }); constructor(parent, props) { super(parent, props); stateB = this.state; } } class ComponentA extends Component { static template = xml`
`; static components = { ComponentB }; state = useState({ fromA: 1 }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("

1b

"); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); stateB.fromB = "c"; await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); defs[0].resolve(); // resolve rendering initiated in A await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); // TODO: is this what we want?? 2b could be ok too defs[1].resolve(); // resolve rendering initiated in B await nextTick(); expect(fixture.innerHTML).toBe("

2c

"); }); test("concurrent renderings scenario 3", async () => { const defB = makeDeferred(); const defsD = [makeDeferred(), makeDeferred()]; let index = 0; let stateC; class ComponentD extends Component { static template = xml``; someValue() { return this.props.fromC; } willUpdateProps() { return defsD[index++]; } } ComponentD.prototype.someValue = jest.fn(ComponentD.prototype.someValue); class ComponentC extends Component { static template = xml``; static components = { ComponentD }; state = useState({ fromC: "c" }); constructor(parent, props) { super(parent, props); stateC = this.state; } } class ComponentB extends Component { static template = xml`

`; static components = { ComponentC }; willUpdateProps() { return defB; } } class ComponentA extends Component { static components = { ComponentB }; static template = xml`
`; state = useState({ fromA: 1 }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("

1c

"); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); stateC.fromC = "d"; await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); defB.resolve(); // resolve rendering initiated in A (still blocked in D) await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); defsD[0].resolve(); // resolve rendering initiated in C (should be ignored) await nextTick(); expect(ComponentD.prototype.someValue).toBeCalledTimes(1); expect(fixture.innerHTML).toBe("

1c

"); defsD[1].resolve(); // completely resolve rendering initiated in A await nextTick(); expect(fixture.innerHTML).toBe("

2d

"); expect(ComponentD.prototype.someValue).toBeCalledTimes(2); }); test("concurrent renderings scenario 4", async () => { const defB = makeDeferred(); const defsD = [makeDeferred(), makeDeferred()]; let index = 0; let stateC; class ComponentD extends Component { static template = xml``; someValue() { return this.props.fromC; } willUpdateProps() { return defsD[index++]; } } ComponentD.prototype.someValue = jest.fn(ComponentD.prototype.someValue); class ComponentC extends Component { static template = xml``; static components = { ComponentD }; state = useState({ fromC: "c" }); constructor(parent, props) { super(parent, props); stateC = this.state; } } class ComponentB extends Component { static template = xml`

`; static components = { ComponentC }; willUpdateProps() { return defB; } } class ComponentA extends Component { static components = { ComponentB }; static template = xml`
`; state = useState({ fromA: 1 }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("

1c

"); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); stateC.fromC = "d"; await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); defB.resolve(); // resolve rendering initiated in A (still blocked in D) await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); defsD[1].resolve(); // completely resolve rendering initiated in A await nextTick(); expect(fixture.innerHTML).toBe("

2d

"); expect(ComponentD.prototype.someValue).toBeCalledTimes(2); defsD[0].resolve(); // resolve rendering initiated in C (should be ignored) await nextTick(); expect(fixture.innerHTML).toBe("

2d

"); expect(ComponentD.prototype.someValue).toBeCalledTimes(2); }); test("concurrent renderings scenario 5", async () => { const defsB = [makeDeferred(), makeDeferred()]; let index = 0; class ComponentB extends Component { static template = xml`

`; someValue() { return this.props.fromA; } willUpdateProps() { return defsB[index++]; } } ComponentB.prototype.someValue = jest.fn(ComponentB.prototype.someValue); class ComponentA extends Component { static components = { ComponentB }; static template = xml`
`; state = useState({ fromA: 1 }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("

1

"); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("

1

"); component.state.fromA = 3; await nextTick(); expect(fixture.innerHTML).toBe("

1

"); defsB[0].resolve(); // resolve first re-rendering (should be ignored) await nextTick(); expect(fixture.innerHTML).toBe("

1

"); expect(ComponentB.prototype.someValue).toBeCalledTimes(1); defsB[1].resolve(); // resolve second re-rendering await nextTick(); expect(fixture.innerHTML).toBe("

3

"); expect(ComponentB.prototype.someValue).toBeCalledTimes(2); }); test("concurrent renderings scenario 6", async () => { const defsB = [makeDeferred(), makeDeferred()]; let index = 0; class ComponentB extends Component { static template = xml`

`; someValue() { return this.props.fromA; } willUpdateProps() { return defsB[index++]; } } ComponentB.prototype.someValue = jest.fn(ComponentB.prototype.someValue); class ComponentA extends Component { static components = { ComponentB }; static template = xml`
`; state = useState({ fromA: 1 }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("

1

"); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("

1

"); component.state.fromA = 3; await nextTick(); expect(fixture.innerHTML).toBe("

1

"); defsB[1].resolve(); // resolve second re-rendering await nextTick(); expect(fixture.innerHTML).toBe("

3

"); expect(ComponentB.prototype.someValue).toBeCalledTimes(2); defsB[0].resolve(); // resolve first re-rendering (should be ignored) await nextTick(); expect(fixture.innerHTML).toBe("

3

"); expect(ComponentB.prototype.someValue).toBeCalledTimes(2); }); test("concurrent renderings scenario 7", async () => { class ComponentB extends Component { static template = xml`

`; state = useState({ fromB: "b" }); someValue() { return this.state.fromB; } async willUpdateProps() { this.state.fromB = "c"; } } ComponentB.prototype.someValue = jest.fn(ComponentB.prototype.someValue); class ComponentA extends Component { static components = { ComponentB }; static template = xml`
`; state = useState({ fromA: 1 }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("

1b

"); expect(ComponentB.prototype.someValue).toBeCalledTimes(1); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("

2c

"); expect(ComponentB.prototype.someValue).toBeCalledTimes(2); }); test("concurrent renderings scenario 8", async () => { const def = makeDeferred(); let stateB; class ComponentB extends Component { static template = xml`

`; state = useState({ fromB: "b" }); constructor(parent, props) { super(parent, props); stateB = this.state; } async willUpdateProps(nextProps) { return def; } } class ComponentA extends Component { static components = { ComponentB }; static template = xml`
`; state = useState({ fromA: 1 }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("

1b

"); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); stateB.fromB = "c"; await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); def.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("

2c

"); }); test("concurrent renderings scenario 9", async () => { // Here is the global idea of this scenario: // A // / \ // B C // | // D // A state is updated, triggering a whole re-rendering // B is async, and blocked // C (and D) are rendered // C state is updated, producing a re-rendering of C and D // this last re-rendering of C should be correctly re-mapped to the whole // re-rendering const def = makeDeferred(); let stateC; class ComponentD extends Component { static template = xml``; } class ComponentC extends Component { static template = xml`

`; static components = { ComponentD }; state = useState({ fromC: "b1" }); constructor(parent, props) { super(parent, props); stateC = this.state; } } class ComponentB extends Component { static template = xml``; willUpdateProps() { return def; } } class ComponentA extends Component { static template = xml`
`; static components = { ComponentB, ComponentC }; state = useState({ fromA: "a1" }); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("
a1a1

a1b1

"); component.state.fromA = "a2"; await nextTick(); expect(fixture.innerHTML).toBe("
a1a1

a1b1

"); stateC.fromC = "b2"; await nextTick(); expect(fixture.innerHTML).toBe("
a1a1

a1b1

"); def.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
a2a2

a2b2

"); }); test("concurrent renderings scenario 10", async () => { // Here is the global idea of this scenario: // A // | // B <- async willUpdateProps // ----- <- conditional (initialy false) // | // C <- async willStart // Render A and B normally // Change the condition on B to trigger a re-rendering with C (async willStart) // Change the state on A to trigger a global re-rendering, which is blocked // in B (async willUpdateProps) // Resolve the willStart of C: the first re-rendering has been cancelled by // the global re-rendering, but handlers waiting for the rendering promise to // resolve might execute and we don't want them to crash/do anything const defB = makeDeferred(); const defC = makeDeferred(); let stateB; class ComponentC extends Component { static template = xml``; willStart() { return defC; } } ComponentC.prototype.__render = jest.fn(ComponentC.prototype.__render); class ComponentB extends Component { static template = xml`

`; state = useState({ hasChild: false }); static components = { ComponentC }; constructor(parent, props) { super(parent, props); stateB = this.state; } willUpdateProps() { return defB; } } class ComponentA extends Component { static template = xml`
`; static components = { ComponentB }; state = useState({ value: 1 }); } const componentA = new ComponentA(); await componentA.mount(fixture); expect(fixture.innerHTML).toBe("

"); stateB.hasChild = true; await nextTick(); expect(fixture.innerHTML).toBe("

"); componentA.state.value = 2; defC.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("

"); defB.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("

2

"); expect(ComponentC.prototype.__render).toHaveBeenCalledTimes(1); }); test("concurrent renderings scenario 11", async () => { // This scenario is the following: we have a component being updated (by props), // and then rendered (render method), but before the willUpdateProps resolves. // We check that in that case, the return value of the render method is a promise // that is resolved when the component is completely rendered (so, properly // remapped to the promise of the ambient rendering) const def = makeDeferred(); let child; class Child extends Component { static template = xml`|`; val = 3; willUpdateProps() { child = this; return def; } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ valA: 1 }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
1|3
"); parent.state.valA = 2; await nextTick(); setTimeout(() => { def.resolve(); }, 20); child.val = 5; await child.render(); expect(fixture.innerHTML).toBe("
2|5
"); }); test("concurrent renderings scenario 12", async () => { // In this scenario, we have a parent component that will be re-rendered // several times simultaneously: // - once in a tick: it will create a new fiber, render it, but will have // to wait for its child (blocking) to be completed // - twice in the next tick: it will twice reuse the same fiber (as it is // rendered but not completed yet) const def = makeDeferred(); class Child extends Component { static template = xml``; willUpdateProps() { return def; } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ val: 1 }); } Parent.prototype.__render = jest.fn(Parent.prototype.__render); const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
1
"); expect(Parent.prototype.__render).toHaveBeenCalledTimes(1); parent.state.val = 2; await nextTick(); expect(fixture.innerHTML).toBe("
1
"); expect(Parent.prototype.__render).toHaveBeenCalledTimes(2); parent.state.val = 3; parent.state.val = 4; await nextTick(); expect(fixture.innerHTML).toBe("
1
"); expect(Parent.prototype.__render).toHaveBeenCalledTimes(3); def.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
4
"); expect(Parent.prototype.__render).toHaveBeenCalledTimes(3); }); test("concurrent renderings scenario 13", async () => { let lastChild; class Child extends Component { static template = xml``; state = useState({ val: 0 }); mounted() { if (lastChild) { lastChild.state.val = 0; } lastChild = this; this.state.val = 1; } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ bool: false }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
0
"); await nextTick(); // wait for changes triggered in mounted to be applied expect(fixture.innerHTML).toBe("
1
"); parent.state.bool = true; await nextTick(); // wait for this change to be applied await nextTick(); // wait for changes triggered in mounted to be applied expect(fixture.innerHTML).toBe("
01
"); }); test("change state and call manually render: no unnecessary rendering", async () => { class Widget extends Component { static template = xml`
`; state = useState({ val: 1 }); } Widget.prototype.__render = jest.fn(Widget.prototype.__render); const widget = new Widget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
1
"); expect(Widget.prototype.__render).toHaveBeenCalledTimes(1); widget.state.val = 2; await widget.render(); expect(fixture.innerHTML).toBe("
2
"); expect(Widget.prototype.__render).toHaveBeenCalledTimes(2); }); }); describe("widget and observable state", () => { test("widget is rerendered when its state is changed", async () => { class TestWidget extends Component { static template = xml`
`; state = useState({ drink: "water" }); } const widget = new TestWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
water
"); widget.state.drink = "beer"; await nextTick(); 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 Child extends Component { static template = xml`
`; constructor(parent, props) { super(parent, props); props.obj.coffee = 2; } } class Parent extends Component { state = useState({ obj: { coffee: 1 } }); static components = { Child }; } const parent = new Parent(); let error; try { await parent.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe('Observed state cannot be changed here! (key: "coffee", val: "2")'); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); }); describe("can deduce template from name", () => { test("can find template if name of component", async () => { class ABC extends Component {} env.qweb.addTemplate("ABC", "Orval"); const abc = new ABC(); await abc.mount(fixture); expect(fixture.innerHTML).toBe("Orval"); }); test("can find template of parent component", async () => { class ABC extends Component {} class DEF extends ABC {} env.qweb.addTemplate("ABC", "Orval"); const def = new DEF(); await def.mount(fixture); expect(fixture.innerHTML).toBe("Orval"); }); test("can find template of parent component, defined by template key", async () => { class ABC extends Component { static template = "Achel"; } class DEF extends ABC {} env.qweb.addTemplate("Achel", "Orval"); const def = new DEF(); 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 Component {} const abc = new ABC(); await abc.mount(fixture); expect(fixture.innerHTML).toBe("Rochefort 8"); abc.destroy(); Component.env = env2; const abc2 = new ABC(); await abc2.mount(fixture); expect(fixture.innerHTML).toBe("Rochefort 10"); }); }); describe("t-model directive", () => { test("basic use, on an input", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "" }); } const comp = new SomeComponent(); 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.template].fn.toString()).toMatchSnapshot(); }); test("basic use, on another key in component", async () => { env.qweb.addTemplates(`
`); class SomeComponent extends Component { some = useState({ text: "" }); } const comp = new SomeComponent(); await comp.mount(fixture); expect(fixture.innerHTML).toBe("
"); const input = fixture.querySelector("input")!; await editInput(input, "test"); expect(comp.some.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 Component { state = useState({ flag: false }); } const comp = new SomeComponent(); 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 Component { state = useState({ choice: "" }); } const comp = new SomeComponent(); 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 Component { state = useState({ color: "" }); } const comp = new SomeComponent(); 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("on a select, initial state", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ color: "red" }); } const comp = new SomeComponent(); await comp.mount(fixture); const select = fixture.querySelector("select")!; expect(select.value).toBe("red"); }); test("on a sub state key", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ something: { text: "" } }); } const comp = new SomeComponent(); await comp.mount(fixture); expect(fixture.innerHTML).toBe("
"); const input = fixture.querySelector("input")!; await editInput(input, "test"); expect(comp.state.something.text).toBe("test"); expect(fixture.innerHTML).toBe("
test
"); expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); }); test(".lazy modifier", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "" }); } const comp = new SomeComponent(); 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.template].fn.toString()).toMatchSnapshot(); }); test(".trim modifier", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "" }); } const comp = new SomeComponent(); 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 () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ number: 0 }); } const comp = new SomeComponent(); 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
"); }); test("in a t-foreach", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState([ { f: false, id: 1 }, { f: false, id: 2 }, { f: false, id: 3 } ]); } const comp = new SomeComponent(); await comp.mount(fixture); expect(fixture.innerHTML).toBe( '
' ); const input = fixture.querySelectorAll("input")[1]!; input.click(); expect(comp.state[1].f).toBe(true); expect(comp.state[0].f).toBe(false); expect(comp.state[2].f).toBe(false); expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); }); test("two inputs in a div with a t-key", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ flag: true }); } const comp = new SomeComponent(); await comp.mount(fixture); expect(fixture.innerHTML).toBe('
'); expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); fixture.querySelector("input")!.value = "asdf"; expect(fixture.querySelector("input")!.value).toBe("asdf"); comp.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe('
'); expect(fixture.querySelector("input")!.value).toBe(""); }); }); 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); class App extends Component { static template = xml`
Red Blue
`; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
Red
"); bus.trigger("some-event"); await nextTick(); expect(fixture.innerHTML).toBe("
Blue
"); }); test("can define specific env for root components", async () => { class App1 extends Component { static template = xml``; } App1.env = { test: 1 }; class App2 extends Component { static template = xml``; } App2.env = { test: 2 }; class App1B extends App1 {} class App2B extends App2 {} App2B.env = { test: 3 }; const app1 = new App1(); const app2 = new App2(); const app1B = new App1B(); const app2B = new App2B(); expect(app1.env.qweb).toBeDefined(); expect(app1.env.test).toBe(1); expect(app2.env.test).toBe(2); expect(app1B.env.test).toBe(1); expect(app2B.env.test).toBe(3); }); }); 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(); const handler = jest.fn(); env.qweb.on("error", null, handler); class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; state = useState({ flag: false }); static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
heyfalse
"); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; expect(handler).toBeCalledTimes(1); }); test("no component catching error lead to full app destruction", async () => { expect.assertions(6); const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { static template = xml`
hey
`; } class App extends Component { static template = xml`
`; static components = { ErrorComponent }; state = useState({ flag: false }); async render() { try { await super.render(); } catch (e) { expect(e.message).toBe("Cannot read property 'this' of undefined"); } } } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
heyfalse
"); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe(""); expect(console.error).toBeCalledTimes(0); 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 (parent mounted)", async () => { const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; expect(handler).toBeCalledTimes(1); }); test("can catch an error in the initial call of a component render function (parent updated)", async () => { const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; state = useState({ flag: false }); static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); 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 Component { constructor(parent) { super(parent); throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); 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(); class ErrorComponent extends Component { static template = xml`
Some text
`; async willStart() { // we wait a little bit to be in a different stack frame await nextTick(); throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test.skip("can catch an error in the mounted call", async () => { // we do not catch error in mounted anymore console.error = jest.fn(); env.qweb.addTemplates(`
Error handled
Some text
`); class ErrorComponent extends Component { mounted() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); }); test.skip("can catch an error in the willPatch call", async () => { // we do not catch error in willPatch anymore const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { static template = xml`
`; willPatch() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; state = useState({ message: "abc" }); static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
abc
abc
"); app.state.message = "def"; await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
def
Error handled
"); expect(console.error).toHaveBeenCalledTimes(1); console.error = consoleError; }); test("a rendering error will reject the mount promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); // we do not catch error in willPatch anymore class App extends Component { static template = xml`
`; } const app = new App(); let error; try { await app.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Cannot read property 'crash' of undefined"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("an error in mounted call will reject the mount promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); class App extends Component { static template = xml`
abc
`; mounted() { throw new Error("boom"); } } const app = new App(); let error; try { await app.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("boom"); expect(fixture.innerHTML).toBe(""); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("an error in willPatch call will reject the render promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); class App extends Component { static template = xml`
`; val = 3; willPatch() { throw new Error("boom"); } } const app = new App(); await app.mount(fixture); app.val = 4; let error; try { await app.render(); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("boom"); expect(fixture.innerHTML).toBe(""); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("an error in patched call will reject the render promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); class App extends Component { static template = xml`
`; val = 3; patched() { throw new Error("boom"); } } const app = new App(); await app.mount(fixture); app.val = 4; let error; try { await app.render(); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("boom"); expect(fixture.innerHTML).toBe(""); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("a rendering error in a sub component will reject the mount promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); // we do not catch error in willPatch anymore class Child extends Component { static template = xml`
`; } class App extends Component { static template = xml`
`; static components = { Child }; } const app = new App(); let error; try { await app.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Cannot read property 'crash' of undefined"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("a rendering error will reject the render promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); // we do not catch error in willPatch anymore class App extends Component { static template = xml`
`; flag = false; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
"); app.flag = true; let error; try { await app.render(); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Cannot read property 'crash' of undefined"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("a rendering error will reject the render promise (with sub components)", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; } let error; try { const parent = new Parent(); await parent.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Cannot read property 'y' of undefined"); }); }); describe("top level sub widgets", () => { test("basic use", async () => { env.qweb.addTemplates(` child `); class Child extends Component {} class Parent extends Component { static components = { Child }; } const parent = new Parent(); 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 Component { state = useState({ val: 1 }); inc() { this.state.val++; } } class Parent extends Component { static components = { Child }; } const parent = new Parent(); 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 () => { class Child extends Component { static template = xml`CHILD 1`; } class OtherChild extends Component { static template = xml`
CHILD 2
`; } class Parent extends Component { static template = xml` `; static components = { Child, OtherChild }; } (env).flag = true; let parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("CHILD 1"); parent.destroy(); (env).flag = false; parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
CHILD 2
"); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); }); test("can select a sub widget, part 2", async () => { class Child extends Component { static template = xml`CHILD 1`; } class OtherChild extends Component { static template = xml`
CHILD 2
`; } class Parent extends Component { static template = xml` `; state = useState({ flag: true }); static components = { Child, OtherChild }; } let parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("CHILD 1"); parent.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
CHILD 2
"); }); test("top level sub widget with a parent", async () => { class ComponentC extends Component { static template = xml`Hello`; } class ComponentB extends Component { static template = xml``; static components = { ComponentC }; } class ComponentA extends Component { static template = xml`
`; static components = { ComponentB }; } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("
Hello
"); }); }); describe("unmounting and remounting", () => { test("widget can be unmounted and remounted", async () => { const steps: string[] = []; class MyWidget extends Component { static template = xml`
Hey
`; async willStart() { steps.push("willstart"); } mounted() { steps.push("mounted"); } willUnmount() { steps.push("willunmount"); } patched() { throw new Error("patched should not be called"); } } const w = new MyWidget(); 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 () => { const steps: string[] = []; class MyWidget extends Component { static template = xml`
Hey
`; async willStart() { steps.push("willstart"); } mounted() { steps.push("mounted"); } willUnmount() { steps.push("willunmount"); } } const w = new MyWidget(); 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[] = []; class Child extends Component { static template = xml` `; state = useState({ n: 2 }); __render(f) { steps.push("render"); return super.__render(f); } willPatch() { steps.push("willPatch"); } patched() { steps.push("patched"); } willUnmount() { steps.push("willUnmount"); this.state.n = 3; } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ val: 1, flag: true }); } const widget = new Parent(); 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 () => { class TestWidget extends Component { static template = xml`
`; state = useState({ val: 1 }); willUnmount() { this.state.val = 3; } } const widget = new TestWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
1
"); widget.unmount(); expect(fixture.innerHTML).toBe(""); await nextTick(); // wait for changes to be detected before remounting await widget.mount(fixture); expect(fixture.innerHTML).toBe("
3
"); // we want to make sure that there are no remaining tasks left at this point. expect(Component.scheduler.tasks.length).toBe(0); }); test("sub component is still active after being unmounted and remounted", async () => { class Child extends Component { static template = xml`

`; state = useState({ value: 1 }); } class Parent extends Component { static components = { Child }; static template = xml`
`; } const w = new Parent(); await w.mount(fixture); expect(fixture.innerHTML).toBe("

1

"); fixture.querySelector("p")!.click(); await nextTick(); expect(fixture.innerHTML).toBe("

2

"); w.unmount(); await nextTick(); await w.mount(fixture); expect(fixture.innerHTML).toBe("

2

"); fixture.querySelector("p")!.click(); await nextTick(); expect(fixture.innerHTML).toBe("

3

"); }); test("change state just before mounting component", async () => { const steps: number[] = []; class TestWidget extends Component { static template = xml`
`; state = useState({ val: 1 }); __render(f) { steps.push(this.state.val); return super.__render(f); } } TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render); const widget = new TestWidget(); widget.state.val = 2; await widget.mount(fixture); expect(fixture.innerHTML).toBe("
2
"); expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1); // unmount and re-mount, as in this case, willStart won't be called, so it's // slightly different widget.unmount(); widget.state.val = 3; await widget.mount(fixture); expect(fixture.innerHTML).toBe("
3
"); expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(2); expect(steps).toEqual([2, 3]); }); test("change state while mounting component", async () => { const steps: number[] = []; class TestWidget extends Component { static template = xml`
`; state = useState({ val: 1 }); __render(f) { steps.push(this.state.val); return super.__render(f); } } TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render); TestWidget.prototype.__patch = jest.fn(TestWidget.prototype.__patch); const widget = new TestWidget(); let prom = widget.mount(fixture); widget.state.val = 2; await prom; expect(fixture.innerHTML).toBe("
2
"); expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1); // unmount and re-mount, as in this case, willStart won't be called, so it's // slightly different widget.unmount(); prom = widget.mount(fixture); widget.state.val = 3; await prom; expect(fixture.innerHTML).toBe("
3
"); expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(3); expect(TestWidget.prototype.__patch).toHaveBeenCalledTimes(2); expect(steps).toEqual([2, 2, 3]); }); test("change state while component is unmounted", async () => { let child; class Child extends Component { static template = xml``; state = useState({ val: "C1" }); constructor(parent, props) { super(parent, props); child = this; } } class Parent extends Component { static components = { Child }; static template = xml`
`; state = useState({ val: "P1" }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
P1C1
"); parent.unmount(); expect(fixture.innerHTML).toBe(""); parent.state.val = "P2"; child.state.val = "C2"; await parent.mount(fixture); expect(fixture.innerHTML).toBe("
P2C2
"); }); test("unmount component during a re-rendering", async () => { const def = makeDeferred(); class Child extends Component { static template = xml``; willUpdateProps() { return def; } } Child.prototype.__render = jest.fn(Child.prototype.__render); class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ val: 1 }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
1
"); expect(Child.prototype.__render).toBeCalledTimes(1); parent.state.val = 2; await nextTick(); expect(fixture.innerHTML).toBe("
1
"); parent.unmount(); expect(fixture.innerHTML).toBe(""); def.resolve(); await nextTick(); expect(fixture.innerHTML).toBe(""); expect(Child.prototype.__render).toBeCalledTimes(1); }); }); describe("dynamic root nodes", () => { test("template with t-if, part 1", async () => { class TestWidget extends Component { static template = xml` hey
abc
`; } const widget = new TestWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("hey"); }); test("template with t-if, part 2", async () => { class TestWidget extends Component { static template = xml` hey
abc
`; } const widget = new TestWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
abc
"); }); test("switching between sub branches dynamically", async () => { class TestWidget extends Component { static template = xml` hey
abc
`; state = useState({ flag: true }); } const widget = new TestWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("hey"); widget.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
abc
"); }); test("switching between sub components dynamically", async () => { class ChildA extends Component { static template = xml`hey`; } class ChildB extends Component { static template = xml`
abc
`; } class TestWidget extends Component { static template = xml` `; static components = { ChildA, ChildB }; state = useState({ flag: true }); } const widget = new TestWidget(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("hey"); widget.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
abc
"); }); }); describe("dynamic t-props", () => { test("basic use", async () => { expect.assertions(4); class Child extends Component { static template = xml` `; constructor(parent, props) { super(parent, props); expect(props).toEqual({ a: 1, b: 2 }); expect(props).not.toBe(widget.some.obj); } } class Parent extends Component { static template = xml`
`; static components = { Child }; some = { obj: { a: 1, b: 2 } }; } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
3
"); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); }); }); describe("support svg components", () => { test("add proper namespace to svg", async () => { class GComp extends Component { static template = xml` `; } class Svg extends Component { static template = xml` `; static components = { GComp }; } const widget = new Svg(); await widget.mount(fixture); expect(fixture.innerHTML).toBe( '' ); }); }); describe("t-raw in components", () => { test("update properly on state changes", async () => { class TestW extends Component { static template = xml`
`; state = useState({ value: "content" }); } const widget = new TestW(); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
content
"); widget.state.value = "other content"; await nextTick(); expect(fixture.innerHTML).toBe("
other content
"); }); test("can render list of t-raw ", async () => { class TestW extends Component { static template = xml`
`; state = useState({ items: ["one", "two", "tree"] }); } const widget = new TestW(); await widget.mount(fixture); expect(fixture.innerHTML).toBe( "
<b>one</b>one<b>two</b>two<b>tree</b>tree
" ); }); }); describe("t-call", () => { test("handlers are properly bound through a t-call", async () => { expect.assertions(3); env.qweb.addTemplate("sub", `

lucas

`); class Parent extends Component { static template = xml`
`; update() { expect(this).toBe(parent); } } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("

lucas

"); fixture.querySelector("p")!.click(); expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); }); test("parent is set within t-call", async () => { env.qweb.addTemplate("sub", ``); let child; class Child extends Component { static template = xml`lucas`; constructor() { super(...arguments); child = this; } } class Parent extends Component { static components = { Child }; static template = xml`
`; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
lucas
"); expect(child.__owl__.parent).toBe(parent); expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); }); test("t-call in t-foreach and children component", async () => { env.qweb.addTemplate("sub", ``); class Child extends Component { static template = xml``; } class Parent extends Component { static components = { Child }; static template = xml`
`; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
abc
"); }); test("parent is set within t-call with no parentNode", async () => { env.qweb.addTemplate("sub", ``); let child; class Child extends Component { constructor() { super(...arguments); child = this; } static template = xml`lucas`; } class Parent extends Component { static components = { Child }; static template = xml``; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("lucas"); expect(child.__owl__.parent).toBe(parent); expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); }); test("handlers with arguments are properly bound through a t-call", async () => { expect.assertions(3); env.qweb.addTemplate("sub", `

lucas

`); class Parent extends Component { static template = xml`
`; update(a) { expect(this).toBe(parent); expect(a).toBe(3); } a = 3; } const parent = new Parent(); await parent.mount(fixture); expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); fixture.querySelector("p")!.click(); }); });