import { Component, Env, mount, STATUS } from "../../src/component/component";
import { EventBus } from "../../src/core/event_bus";
import { useRef, useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import {
editInput,
makeDeferred,
makeTestEnv,
makeTestFixture,
nextMicroTick,
nextTick,
normalize,
} 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`
");
});
test("display a nice message if mounted on a non existing node", async () => {
class SomeWidget extends Component {
static template = xml`
content
`;
}
let error;
try {
await mount(SomeWidget, { target: 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``;
}
let error;
try {
await mount(SomeWidget, { target: 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`
`);
});
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`
`);
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``;
setup() {
steps.push("setup");
}
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(["setup", "willstart", "mounted"]);
widget.state.ok = false;
await nextTick();
expect(steps).toEqual(["setup", "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`
");
});
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("can switch between dynamic components without the need for a t-key", 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`
`;
static components = { A, B };
state = useState({ child: "A" });
}
const app = await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe("
");
expect(QWeb.TEMPLATES[App.template].fn.toString()).toMatchSnapshot();
});
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("don't fallback to global/component's registry if widget defined in the instance's context", async () => {
QWeb.registerComponent("WidgetB", WidgetB); // should not use this widget
env.qweb.addTemplate("ParentWidget", `
`);
env.qweb.addTemplate("ComponentWidgetB", `Belgium`); // should not use this widget either
env.qweb.addTemplate("InstanceWidgetB", `Chocolate`); // should use this
class ComponentWidgetB extends Component {}
class InstanceWidgetB extends Component {}
class ParentWidget extends Component {
static components = { WidgetB: ComponentWidgetB };
WidgetB = InstanceWidgetB;
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
Chocolate
");
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", `
");
});
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", `
`)
);
});
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",
`
`)
);
});
test("list of sub components inside other nodes", async () => {
// this confuses the patching algorithm...
env.qweb.addTemplate("ChildWidget", `child`);
env.qweb.addTemplates(`
"
);
});
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("
`);
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("t-set with a body expression can be used as textual prop", async () => {
class Child extends Component {
static template = xml``;
}
class Parent extends Component {
static components = { Child };
static template = xml`
42
`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
42
");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("t-set with a body expression can be passed in props, and then t-raw", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
43
`;
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("bare function calls in arrow function has rendering context as 'this'", async () => {
expect.assertions(7);
let child, parent;
class Child extends Component {
setup() {
child = this;
}
}
env.qweb.addTemplate("Child", ``);
class Parent extends Component {
static components = { Child };
state = useState({ val: 42 });
setup() {
parent = this;
}
setValue(value) {
// 'this' is the rendering context, NOT the instance
expect(this).not.toBe(parent);
// the state in the rendering context should be the same as the instance's
expect(this.state).toBe(parent.state);
expect((this as any).ctxVal).toBe(2);
this.state.val = value;
}
}
env.qweb.addTemplate(
"Parent",
`
`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
");
});
test("t-set in t-if", async () => {
// Weird that code block within 'if' leaks outside of it
// Python does the same
class SomeWidget extends Component {
static template = xml`
`;
state = { flag: "if" };
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("
"
);
expect(widget.otherState.vals).toStrictEqual([]);
const buttons = fixture.querySelectorAll("button");
buttons[0].click();
buttons[1].click();
expect(widget.otherState.vals).toStrictEqual(["0_0", "1_1"]);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
});
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("
`);
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(`
");
});
});
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("