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(
"
"
);
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("can define and call slots using old t-set keyword", async () => {
// NOTE: this test should be removed once we stop supporting the t-set directive
// for defining slot content.
class Dialog extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml`
header
footer
`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe(
""
);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(env.qweb.templates[Dialog.template].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(`
do something
`);
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 do something
'
);
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'1 do something
'
);
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`
do something
`;
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 do something
'
);
parent.button.el!.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'1 do something
'
);
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("");
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("");
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("");
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("");
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-slot (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("");
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:
Inc[ ]
`);
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("");
(fixture.querySelector("button")).click();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
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(`1 2
`);
});
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("A0 A1
");
await nextTick(); // wait for the changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("B0 B1
");
});
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("A0 A1
");
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("B0 B1
");
});
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("");
});
test("t-set t-value in a slot", async () => {
class Dialog extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml`
`;
static components = { Dialog };
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("dash
");
expect(env.qweb.templates[Dialog.template].fn.toString()).toMatchSnapshot();
});
});