import { App, Component, mount, status, toRaw, useState, xml } from "../../src";
import { elem, makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
import { markup } from "../../src/runtime/utils";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("basics", () => {
test("can mount a simple component", async () => {
class Test extends Component {
static template = xml`simple vnode `;
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("simple vnode ");
expect(elem(component)).toEqual(fixture.querySelector("span"));
});
test("cannot mount on a documentFragment", async () => {
class SomeWidget extends Component {
static template = xml`
content
`;
}
let error: Error;
try {
await mount(SomeWidget, document.createDocumentFragment() as any);
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount component: the target is not a valid DOM element");
});
test("can mount a simple component with props", async () => {
class Test extends Component {
static template = xml` `;
}
const app = new App(Test, { props: { value: 3 } });
const component = await app.mount(fixture);
expect(fixture.innerHTML).toBe("3 ");
expect(elem(component)).toEqual(fixture.querySelector("span"));
});
test("can mount a component with just some text", async () => {
class Test extends Component {
static template = xml`just text`;
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("just text");
expect(elem(component)).toBeInstanceOf(Text);
});
test("can mount a component with no text", async () => {
class Test extends Component {
static template = xml` `;
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("");
expect(elem(component)).toBeInstanceOf(Text);
});
test("can mount a simple component with multiple roots", async () => {
class Test extends Component {
static template = xml`
`;
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("
");
expect(elem(component).tagName).toBe("SPAN");
});
test("component with dynamic content can be updated", async () => {
class Test extends Component {
static template = xml` `;
value = 1;
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("1 ");
component.value = 2;
component.render();
await nextTick();
expect(fixture.innerHTML).toBe("2 ");
});
test("updating a component with t-foreach as root", async () => {
class Test extends Component {
static template = xml`
`;
items = ["one", "two", "three"];
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("onetwothree");
component.items = ["two", "three", "one"];
component.render();
await nextTick();
expect(fixture.innerHTML).toBe("twothreeone");
});
test("props is set on root component", async () => {
expect.assertions(2);
const p = {};
class Test extends Component {
static template = xml`simple vnode `;
setup() {
expect(toRaw(this.props)).not.toBe(p);
}
}
const app = new App(Test, { props: p });
await app.mount(fixture);
});
test("props value are own property of props object", async () => {
expect.assertions(2);
const p = { a: 1 };
class Test extends Component {
static template = xml`simple vnode `;
setup() {
expect(Object.prototype.hasOwnProperty.call(this.props, "a")).toBe(true);
}
}
const app = new App(Test, { props: p });
await app.mount(fixture);
});
test("props value are own property of props object, even with default values", async () => {
expect.assertions(3);
const p = { a: 1 };
class Test extends Component {
static template = xml`simple vnode `;
static defaultProps = { b: 1 };
setup() {
expect(Object.prototype.hasOwnProperty.call(this.props, "a")).toBe(true);
expect(Object.prototype.hasOwnProperty.call(this.props, "b")).toBe(true);
}
}
const app = new App(Test, { props: p });
await app.mount(fixture);
});
test("some simple sanity checks (el/status)", async () => {
expect.assertions(3);
class Test extends Component {
static template = xml`simple vnode `;
setup() {
expect(status(this)).toBe("new");
}
}
const test = await mount(Test, fixture);
expect(status(test)).toBe("mounted");
});
test("throws if mounting on target=null", async () => {
class Test extends Component {
static template = xml`simple vnode `;
}
let error: Error;
try {
await mount(Test, null as any);
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount component: the target is not a valid DOM element");
});
test("a component cannot be mounted in a detached node", async () => {
class Test extends Component {
static template = xml`
`;
}
let error: Error;
try {
await mount(Test, document.createElement("div"));
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
});
test("a component cannot be mounted in a detached node (even if node is detached later)", async () => {
const warn = console.warn;
console.warn = jest.fn();
class Test extends Component {
static template = xml`
`;
}
let error: Error;
const prom = mount(Test, fixture);
await Promise.resolve();
fixture.remove();
try {
await prom;
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
expect(console.warn).toBeCalledTimes(1);
console.warn = warn;
});
test("crashes if it cannot find a template", async () => {
class Test extends Component {
static template = "wrongtemplate";
}
let error: Error;
try {
await mount(Test, fixture);
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe('Missing template: "wrongtemplate" (for component "Test")');
});
test("class component with dynamic text", async () => {
class Test extends Component {
static template = xml`My value: `;
value = 42;
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe("My value: 42 ");
});
test("Multi root component", async () => {
class Test extends Component {
static template = xml`1 text2 `;
value = 42;
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe(`1 text2 `);
});
test("a component inside a component", async () => {
class Child extends Component {
static template = xml`simple vnode
`;
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("simple vnode
");
});
test("a class component inside a class component, no external dom", async () => {
class Child extends Component {
static template = xml`simple vnode
`;
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("simple vnode
");
});
test("simple component with a dynamic text", async () => {
class Test extends Component {
static template = xml`
`;
value = 3;
}
const test = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("3
");
test.value = 5;
test.render();
await nextTick();
expect(fixture.innerHTML).toBe("5
");
});
test("simple component, useState", async () => {
class Test extends Component {
static template = xml`
`;
state = useState({ value: 3 });
}
const test = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("3
");
test.state.value = 5;
await nextTick();
expect(fixture.innerHTML).toBe("5
");
});
test("two child components", async () => {
class Child extends Component {
static template = xml`simple vnode
`;
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("simple vnode
simple vnode
");
});
test("class parent, class child component with props", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("42
");
});
test("parent, child and grandchild", async () => {
class GrandChild extends Component {
static template = xml`hey
`;
}
class Child extends Component {
static template = xml` `;
static components = { GrandChild };
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("hey
");
});
test("zero or one child components", async () => {
class Child extends Component {
static template = xml`simple vnode
`;
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ hasChild: false });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
parent.state.hasChild = true;
await nextTick();
expect(fixture.innerHTML).toBe("simple vnode
");
});
test("text after a conditional component", async () => {
class Child extends Component {
static template = xml`simple vnode
`;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ hasChild: false, text: "1" });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1
");
parent.state.hasChild = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
parent.state.hasChild = false;
parent.state.text = "2";
await nextTick();
expect(fixture.innerHTML).toBe("2
");
});
test("can be clicked on and updated", async () => {
class Counter extends Component {
static template = xml`
Inc
`;
state = useState({
counter: 0,
});
}
await mount(Counter, fixture);
expect(fixture.innerHTML).toBe("0Inc
");
const button = fixture.getElementsByTagName("button")[0];
button.click();
await nextTick();
expect(fixture.innerHTML).toBe("1Inc
");
});
// TODO: rename
test("rerendering a widget with a sub widget", async () => {
class Counter extends Component {
static template = xml`
Inc
`;
state = useState({
counter: 0,
});
}
class Parent extends Component {
static template = xml` `;
static components = { Counter };
}
const parent = await mount(Parent, fixture);
const button = fixture.getElementsByTagName("button")[0];
button.click();
await nextTick();
expect(fixture.innerHTML).toBe("1Inc
");
await parent.render();
expect(fixture.innerHTML).toBe("1Inc
");
});
test("can handle empty props", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("
");
});
test("child can be updated", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({
counter: 0,
});
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("0");
parent.state.counter = 1;
await nextTick();
expect(fixture.innerHTML).toBe("1");
});
test("higher order components parent and child", async () => {
class ChildA extends Component {
static template = xml`a
`;
}
class ChildB extends Component {
static template = xml`b `;
}
class Child extends Component {
static template = xml` `;
static components = { ChildA, ChildB };
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ child: "a" });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(`a
`);
parent.state.child = "b";
await nextTick();
expect(fixture.innerHTML).toBe(`b `);
});
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` `;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("2
");
});
test("do not remove previously rendered dom if not necessary", async () => {
class SomeComponent extends Component {
static template = xml`
`;
}
const widget = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe(`
`);
fixture.querySelector("div")!.appendChild(document.createElement("span"));
expect(fixture.innerHTML).toBe(`
`);
await widget.render();
expect(fixture.innerHTML).toBe(`
`);
});
test("do not remove previously rendered dom if not necessary, variation", async () => {
class SomeComponent extends Component {
static template = xml`
h1 `;
state = useState({ value: 1 });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe(`
h1 1 `);
fixture.querySelector("h1")!.appendChild(document.createElement("p"));
expect(fixture.innerHTML).toBe("");
comp.state.value++;
await nextTick();
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 };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("child child
");
});
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 };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
});
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 };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("child
");
});
test("widget after a t-foreach", async () => {
class SomeComponent extends Component {
static template = xml`
`;
}
class Test extends Component {
static template = xml`txt
`;
static components = { SomeComponent };
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe("");
});
test("t-if works with t-component", async () => {
class Child extends Component {
static template = xml`hey `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("hey
");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("
");
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("hey
");
});
test("t-else works with t-component", async () => {
class Child extends Component {
static template = xml`hey `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("hey
");
});
test("t-elif works with t-component", async () => {
class Child extends Component {
static template = xml`hey `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("hey
");
});
test("t-else with empty string works with t-component", async () => {
class Child extends Component {
static template = xml`hey `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("hey
");
});
test("sub components between t-ifs", async () => {
// this confuses the patching algorithm...
class Child extends Component {
static template = xml`child `;
}
class Parent extends Component {
static template = xml`
hey
noo
test
`;
static components = { Child };
state = useState({ flag: false });
}
const parent = await mount(Parent, fixture);
const child = Object.values(parent.__owl__.children)[0].component;
expect(fixture.innerHTML).toBe(`
noo child `);
parent.state.flag = true;
await nextTick();
expect(Object.values(parent.__owl__.children)[0].component).toBe(child);
expect(status(child)).toBe("mounted");
expect(fixture.innerHTML).toBe(
`
hey child test `
);
});
test("list of two sub components inside other nodes", async () => {
class SubWidget extends Component {
static template = xml`asdf `;
}
class Parent extends Component {
static template = xml`
`;
static components = { SubWidget };
state = useState({ blips: [{ a: "a", id: 1 }] });
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
});
// TODO: rename
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.
class Child extends Component {
static template = xml`abcdef `;
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ flag: false });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("abc ");
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("abcdef ");
});
test("can inject values in tagged templates", async () => {
const SUBTEMPLATE = xml` `;
class Parent extends Component {
static template = xml` `;
state = useState({ n: 42 });
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("42 ");
});
// Depends on t-props
test("update props of component without concrete own node", async () => {
class Custom extends Component {
static template = xml`
__
`;
}
class Child extends Component {
static components = { Custom };
static template = xml`
`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
`;
childProps = {
key: 1,
subKey: 1,
};
}
const parent = await mount(Parent, fixture);
expect(fixture.textContent!.trim()).toBe("1__1");
// First step: change the Custom's instance
Object.assign(parent.childProps, {
subKey: 2,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("1__2");
// Second step, change both Child's and Custom's instance
Object.assign(parent.childProps, {
key: 2,
subKey: 3,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("2__3");
});
test("component children doesn't leak (if case)", async () => {
class Child extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static components = { Child };
static template = xml` `;
ifVar = true;
}
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
parent.ifVar = false;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(0);
expect(["Child:willUnmount", "Child:willDestroy"]).toBeLogged();
});
test("component children doesn't leak (t-key case)", async () => {
// This test should encompass the t-foreach and t-call cases too (because they also use a flavor of some key)
class Child extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static components = { Child };
static template = xml` `;
keyVar = 1;
}
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
parent.keyVar = 2;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:willUnmount",
"Child:willDestroy",
"Child:mounted",
]).toBeLogged();
});
test("GrandChild display is controlled by its GrandParent", async () => {
class GrandChild extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Child extends Component {
static components = { GrandChild };
static template = xml` `;
}
class Parent extends Component {
static template = xml` `;
myComp = Child;
displayGrandChild = true;
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("
");
expect([
"GrandChild:setup",
"GrandChild:willStart",
"GrandChild:willRender",
"GrandChild:rendered",
"GrandChild:mounted",
]).toBeLogged();
parent.displayGrandChild = false;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(["GrandChild:willUnmount", "GrandChild:willDestroy"]).toBeLogged();
});
});
describe("mount targets", () => {
test("can mount a component (with position='first-child')", async () => {
class Root extends Component {
static template = xml`app
`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App(Root);
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("app
");
});
test("can mount a component (with default position='last-child')", async () => {
class Root extends Component {
static template = xml`app
`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App(Root);
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("app
");
});
test("mount function: can mount a component (with default position='last-child')", async () => {
class Root extends Component {
static template = xml`app
`;
}
const span = document.createElement("span");
fixture.appendChild(span);
await mount(Root, fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("app
");
});
test("default mount option is 'last-child'", async () => {
class Root extends Component {
static template = xml`app
`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App(Root);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("app
");
});
});
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 };
}
await mount(Svg, fixture);
expect(fixture.innerHTML).toBe(
' '
);
});
});
describe("t-out in components", () => {
test("update properly on state changes", async () => {
class Test extends Component {
static template = xml`
`;
state = useState({ value: markup("content ") });
}
const component = await mount(Test, fixture);
markup("prout");
expect(fixture.innerHTML).toBe("content
");
component.state.value = markup("other content ");
await nextTick();
expect(fixture.innerHTML).toBe("other content
");
});
test("can render list of t-out ", async () => {
class Test extends Component {
static template = xml`
`;
state = useState({
items: [markup("one "), markup("two "), markup("tree ")],
});
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe(
"<b>one</b>one <b>two</b>two <b>tree</b>tree
"
);
});
test("can switch the contents of two t-out repeatedly", async () => {
class Test extends Component {
static template = xml`
`;
state = useState({
a: markup("1
"),
b: markup("2
"),
});
reverse() {
const { state } = this;
[state.a, state.b] = [state.b, state.a];
}
}
const comp = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("1
2
");
comp.reverse();
await nextTick();
expect(fixture.innerHTML).toBe("2
1
");
comp.reverse();
await nextTick();
expect(fixture.innerHTML).toBe("1
2
");
});
});