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 { makeTestFixture, makeTestEnv, nextTick } 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(); Component.env = env; }); afterEach(() => { fixture.remove(); }); function children(w: Component): Component[] { const childrenMap = w.__owl__.children; return Object.keys(childrenMap).map(id => childrenMap[id]); } //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ describe("t-slot directive", () => { test("can define and call slots", async () => { env.qweb.addTemplates(`
header footer
`); class Dialog extends Component {} class Parent extends Component { static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe( "
header
footer
" ); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot(); expect(QWeb.slots["1_header"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot(); }); test("named slots can define a default content", async () => { class Dialog extends Component { static template = xml` default content `; } class Parent extends Component { static template = xml`
`; static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
default content
"); expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot(); }); test("dafault slots can define a default content", async () => { class Dialog extends Component { static template = xml` default content `; } class Parent extends Component { static template = xml`
`; static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
default content
"); expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot(); }); test("default content is not rendered if slot is provided", async () => { class Dialog extends Component { static template = xml` default content `; } class Parent extends Component { static template = xml`
hey
`; static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
hey
"); }); test("default content is not rendered if named slot is provided", async () => { class Dialog extends Component { static template = xml` default content `; } class Parent extends Component { static template = xml`
hey
`; static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
hey
"); }); test("slots are rendered with proper context", async () => { env.qweb.addTemplates(`
`); class Dialog extends Component {} class Parent extends Component { static components = { Dialog }; state = useState({ val: 0 }); doSomething() { this.state.val++; } } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe( '
0
' ); fixture.querySelector("button")!.click(); await nextTick(); expect(fixture.innerHTML).toBe( '
1
' ); expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot(); }); test("slots are rendered with proper context, part 2", async () => { env.qweb.addTemplates(`
  • User
  • `); class Link extends Component {} class App extends Component { state = useState({ users: [ { id: 1, name: "Aaron" }, { id: 2, name: "David" } ] }); static components = { Link }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe( '' ); expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot(); // test updateprops here app.state.users[1].name = "Mathieu"; await nextTick(); expect(fixture.innerHTML).toBe( '' ); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("slots are rendered with proper context, part 3", async () => { env.qweb.addTemplates(`
  • `); class Link extends Component {} class App extends Component { state = useState({ users: [ { id: 1, name: "Aaron" }, { id: 2, name: "David" } ] }); static components = { Link }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe( '' ); expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot(); // test updateprops here app.state.users[1].name = "Mathieu"; await nextTick(); expect(fixture.innerHTML).toBe( '' ); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("slots are rendered with proper context, part 4", async () => { env.qweb.addTemplates(`
    `); class Link extends Component {} class App extends Component { state = useState({ user: { id: 1, name: "Aaron" } }); static components = { Link }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe(''); expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot(); // test updateprops here app.state.user.name = "David"; await nextTick(); expect(fixture.innerHTML).toBe(''); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("refs are properly bound in slots", async () => { class Dialog extends Component { static template = xml``; } class Parent extends Component { static template = xml`
    `; static components = { Dialog }; state = useState({ val: 0 }); button = useRef("myButton"); doSomething() { this.state.val++; } } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe( '
    0
    ' ); parent.button.el!.click(); await nextTick(); expect(fixture.innerHTML).toBe( '
    1
    ' ); expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot(); }); test("content is the default slot", async () => { env.qweb.addTemplates(`
    sts rocks
    `); class Dialog extends Component {} class Parent extends Component { static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    sts rocks
    "); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("default slot work with text nodes", async () => { env.qweb.addTemplates(`
    sts rocks
    `); class Dialog extends Component {} class Parent extends Component { static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    sts rocks
    "); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("multiple roots are allowed in a named slot", async () => { env.qweb.addTemplates(`
    sts rocks
    `); class Dialog extends Component {} class Parent extends Component { static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    stsrocks
    "); expect(QWeb.slots["1_content"].toString()).toMatchSnapshot(); }); test("multiple roots are allowed in a default slot", async () => { env.qweb.addTemplates(`
    sts rocks
    `); class Dialog extends Component {} class Parent extends Component { static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    stsrocks
    "); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("missing slots are ignored", async () => { env.qweb.addTemplates(`
    some content
    `); class Dialog extends Component {} class Parent extends Component { static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    some content
    "); }); test("t-debug on a t-set (defining a slot)", async () => { const consoleLog = console.log; console.log = jest.fn(); env.qweb.addTemplates(`
    abc
    `); class Dialog extends Component {} class Parent extends Component { static components = { Dialog }; } const parent = new Parent(); await parent.mount(fixture); expect(console.log).toHaveBeenCalledTimes(0); console.log = consoleLog; }); test("slot preserves properly parented relationship", async () => { env.qweb.addTemplates(`
    Grand Child
    `); class Child extends Component {} class GrandChild extends Component {} class Parent extends Component { static components = { Child, GrandChild }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    Grand Child
    "); const parentChildren = children(parent); expect(parentChildren.length).toBe(1); expect(parentChildren[0]).toBeInstanceOf(Child); const childrenChildren = children(parentChildren[0]); expect(childrenChildren.length).toBe(1); expect(childrenChildren[0]).toBeInstanceOf(GrandChild); }); test("nested slots: evaluation context and parented relationship", async () => { let slot; class Slot extends Component { static template = xml``; constructor(parent, props) { super(parent, props); slot = this; } } class GrandChild extends Component { static template = xml`
    `; } class Child extends Component { static components = { GrandChild }; static template = xml` `; } class Parent extends Component { static components = { Child, Slot }; static template = xml``; state = useState({ val: 3 }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    3
    "); expect(slot.__owl__.parent).toBeInstanceOf(GrandChild); }); test("slot are properly rendered if inner props are changed", async () => { env.qweb.addTemplates(`
    SC:
    `); class SomeComponent extends Component {} class GenericComponent extends Component {} class App extends Component { static components = { GenericComponent, SomeComponent }; state = useState({ val: 4 }); inc() { this.state.val++; } } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
    SC:4
    "); (fixture.querySelector("button")).click(); await nextTick(); expect(fixture.innerHTML).toBe("
    SC:5
    "); }); test("slots and wrapper components", async () => { class Link extends Component { static template = xml` `; } class A extends Component { static template = xml`hey`; static components = { Link: Link }; } const a = new A(); await a.mount(fixture); expect(fixture.innerHTML).toBe(`hey`); }); test("template can just return a slot", async () => { class Child extends Component { static template = xml``; } class SlotComponent extends Component { static template = xml``; } class Parent extends Component { static template = xml`
    `; static components = { SlotComponent, Child }; state = useState({ value: 3 }); } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    3
    "); expect(QWeb.TEMPLATES[SlotComponent.template].fn.toString()).toMatchSnapshot(); parent.state.value = 5; await nextTick(); expect(fixture.innerHTML).toBe("
    5
    "); }); test("multiple slots containing components", async () => { class C extends Component { static template = xml``; } class B extends Component { static template = xml`
    `; } class A extends Component { static template = xml` `; static components = { B, C }; } const a = new A(); await a.mount(fixture); expect(fixture.innerHTML).toBe(`
    12
    `); }); test("slots in t-foreach and re-rendering", async () => { class Child extends Component { static template = xml``; state = useState({ val: "A" }); mounted() { this.state.val = "B"; } } class Parent extends Component { static components = { Child }; static template = xml`
    `; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    A0A1
    "); await nextTick(); // wait for the changes triggered in mounted to be applied expect(fixture.innerHTML).toBe("
    B0B1
    "); }); test("slots in t-foreach with t-set and re-rendering", 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
    "); }); test("nested slots in same template", async () => { let child, child2, child3; class Child extends Component { static template = xml`
    `; constructor(parent, props) { super(parent, props); child = this; } } class Child2 extends Component { static template = xml` `; constructor(parent, props) { super(parent, props); child2 = this; } } class Child3 extends Component { static template = xml` Child 3`; constructor(parent, props) { super(parent, props); child3 = this; } } class Parent extends Component { static components = { Child, Child2, Child3 }; static template = xml` `; } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe( '
    Child 3
    ' ); expect(child3.__owl__.parent).toStrictEqual(child2); expect(child2.__owl__.parent).toStrictEqual(child); expect(child.__owl__.parent).toStrictEqual(widget); }); test("t-slot nested within another slot", async () => { let portal, modal, child3; class Child3 extends Component { static template = xml` Child 3`; constructor(parent, props) { super(parent, props); child3 = this; } } class Modal extends Component { static template = xml` `; constructor(parent, props) { super(parent, props); modal = this; } } class Portal extends Component { static template = xml` `; constructor(parent, props) { super(parent, props); portal = this; } } class Dialog extends Component { static components = { Modal, Portal }; static template = xml` `; } class Parent extends Component { static components = { Child3, Dialog }; static template = xml` `; } const widget = new Parent(); await widget.mount(fixture); expect(fixture.innerHTML).toBe( 'Child 3' ); expect(child3.__owl__.parent).toStrictEqual(portal); expect(portal.__owl__.parent).toStrictEqual(modal); }); test("t-slot supports many instances", async () => { let child3; class Child3 extends Component { static template = xml` Child 3`; constructor(parent, props) { super(parent, props); child3 = this; } } class Dialog extends Component { static template = xml` `; } class Parent extends Component { static components = { Child3, Dialog }; static template = xml` `; state = { lol: "k" }; } const widget = new Parent(); await widget.mount(fixture); expect(child3.props.val).toBe("k"); const widget_1 = new Parent(); widget_1.state.lol = "m"; await widget_1.mount(fixture); expect(child3.props.val).toBe("m"); }); test("slots in slots, with vars", async () => { class B extends Component { static template = xml``; } class A extends Component { static template = xml`
    `; static components = { B }; } class Parent extends Component { static template = xml` `; static components = { A }; state = { name: "aaron" }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("

    heyaaron

    "); }); });