mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
0bc9573a8a
When fixing the absence of capture for arrow functions passed as props, we unintentionally introduced a breaking change: bare function calls in the arrow functions used to be called with the rendering context as their this value and this was no longer the case. This commit fixes that by intentionally not capturing the value of functions that are called withing the arrow function.
4501 lines
134 KiB
TypeScript
4501 lines
134 KiB
TypeScript
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", `<div>Hello<t t-component="b"/></div>`);
|
|
env.qweb.addTemplate("WidgetB", `<div>world</div>`);
|
|
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`<div>content</div>`;
|
|
}
|
|
await mount(SomeWidget, { target: fixture });
|
|
expect(fixture.innerHTML).toBe("<div>content</div>");
|
|
});
|
|
|
|
test("can be mounted with props", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`<div><t t-esc="props.content"/></div>`;
|
|
}
|
|
await mount(SomeWidget, { target: fixture, props: { content: "foo" } });
|
|
expect(fixture.innerHTML).toBe("<div>foo</div>");
|
|
});
|
|
|
|
test("can be mounted on a documentFragment", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`<div>content</div>`;
|
|
}
|
|
const widget = await mount(SomeWidget, { target: document.createDocumentFragment() });
|
|
expect(fixture.innerHTML).toBe("");
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>content</div>");
|
|
});
|
|
|
|
test("display a nice message if mounted on a non existing node", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`<div>content</div>`;
|
|
}
|
|
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`<t/>`;
|
|
}
|
|
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`
|
|
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
|
|
state = useState({
|
|
counter: 0,
|
|
});
|
|
}
|
|
|
|
const counter = await mount(Counter, { target: fixture });
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>");
|
|
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
|
|
button.click();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
|
|
});
|
|
|
|
test("support for callable expression in event handler", async () => {
|
|
class Counter extends Component {
|
|
static template = xml`
|
|
<div><t t-esc="state.value"/><input type="text" t-on-input="obj.onInput"/></div>`;
|
|
state = useState({ value: "" });
|
|
obj = { onInput: (ev) => (this.state.value = ev.target.value) };
|
|
}
|
|
|
|
const counter = await mount(Counter, { target: fixture });
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(`<div><input type="text"></div>`);
|
|
const input = (<HTMLElement>counter.el).getElementsByTagName("input")[0];
|
|
input.value = "test";
|
|
input.dispatchEvent(new Event("input"));
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(`<div>test<input type="text"></div>`);
|
|
});
|
|
|
|
test("can handle empty props", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child val=""/></div>`;
|
|
static components = { Child };
|
|
}
|
|
|
|
await mount(Parent, { target: fixture });
|
|
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
|
|
expect(fixture.innerHTML).toBe("<div><span></span></div>");
|
|
});
|
|
|
|
test("can be clicked on and updated if not in DOM", async () => {
|
|
class Counter extends Component {
|
|
static template = xml`
|
|
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
|
|
state = useState({
|
|
counter: 0,
|
|
});
|
|
}
|
|
|
|
const target = document.createElement("div");
|
|
const counter = await mount(Counter, { target: target });
|
|
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
|
|
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
|
|
button.click();
|
|
await nextTick();
|
|
expect(target.innerHTML).toBe("<div>1<button>Inc</button></div>");
|
|
expect(counter.state.counter).toBe(1);
|
|
});
|
|
|
|
test("widget style and classname", async () => {
|
|
class StyledWidget extends Component {
|
|
static template = xml`
|
|
<div style="font-weight:bold;" class="some-class">world</div>
|
|
`;
|
|
}
|
|
await mount(StyledWidget, { target: fixture });
|
|
expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`);
|
|
});
|
|
|
|
test("changing state before first render does not trigger a render", async () => {
|
|
const steps: string[] = [];
|
|
class TestW extends Component {
|
|
static template = xml`<div/>`;
|
|
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");
|
|
}
|
|
}
|
|
await mount(TestW, { target: 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`<span>W</span>`;
|
|
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`<div><TestW t-if="state.flag"/></div>`;
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
expect(steps).toEqual([]);
|
|
|
|
parent.state.flag = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>W</span></div>");
|
|
expect(steps).toEqual(["__render", "mounted"]);
|
|
});
|
|
|
|
test("render method wait until rendering is done", async () => {
|
|
class TestW extends Component {
|
|
static template = xml`<div><t t-esc="state.drinks"/></div>`;
|
|
state = { drinks: 1 };
|
|
}
|
|
const widget = new TestW();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
|
|
widget.state.drinks = 2;
|
|
|
|
const renderPromise = widget.render();
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
await renderPromise;
|
|
expect(fixture.innerHTML).toBe("<div>2</div>");
|
|
});
|
|
|
|
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`<div/>`;
|
|
}
|
|
const widget = new SomeComponent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe(`<div></div>`);
|
|
widget.el!.appendChild(document.createElement("span"));
|
|
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
|
|
widget.render(); // FIXME?
|
|
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
|
|
});
|
|
|
|
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`<span>child</span>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<Child />
|
|
<Child t-key="4"/>
|
|
</div>
|
|
`;
|
|
static components = { Child };
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>child</span><span>child</span></div>");
|
|
});
|
|
|
|
test("reconciliation alg works for t-foreach in t-foreach", async () => {
|
|
const warn = console.warn;
|
|
console.warn = () => {};
|
|
class Child extends Component {
|
|
static template = xml`<div><t t-esc="props.blip"/></div>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="state.s" t-as="section">
|
|
<t t-foreach="section.blips" t-as="blip">
|
|
<Child blip="blip"/>
|
|
</t>
|
|
</t>
|
|
</div>`;
|
|
static components = { Child };
|
|
state = { s: [{ blips: ["a1", "a2"] }, { blips: ["b1"] }] };
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>a1</div><div>a2</div><div>b1</div></div>");
|
|
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`<div><t t-esc="props.row + '_' + props.col"/></div>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<p t-foreach="state.rows" t-as="row" t-key="row">
|
|
<p t-foreach="state.cols" t-as="col" t-key="col">
|
|
<Child row="row" col="col"/>
|
|
</p>
|
|
</p>
|
|
</div>`;
|
|
static components = { Child };
|
|
state = useState({ rows: [1, 2], cols: ["a", "b"] });
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><p><p><div>1_a</div></p><p><div>1_b</div></p></p><p><p><div>2_a</div></p><p><div>2_b</div></p></p></div>"
|
|
);
|
|
widget.state.rows = [2, 1];
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><p><p><div>2_a</div></p><p><div>2_b</div></p></p><p><p><div>1_a</div></p><p><div>1_b</div></p></p></div>"
|
|
);
|
|
});
|
|
|
|
test("same t-keys in two different places", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.blip"/></span>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<div><Child t-key="1" blip="'1'"/></div>
|
|
<div><Child t-key="1" blip="'2'"/></div>
|
|
</div>`;
|
|
static components = { Child };
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div><span>1</span></div><div><span>2</span></div></div>");
|
|
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`<span>child</span>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<Child t-if="false" t-key="'str'"/>
|
|
<Child/>
|
|
</div>`;
|
|
static components = { Child };
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>child</span></div>");
|
|
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
describe("lifecycle hooks", () => {
|
|
test("willStart hook is called", async () => {
|
|
let willstart = false;
|
|
class HookWidget extends Component {
|
|
static template = xml`<div/>`;
|
|
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`<div/>`;
|
|
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`<div/>`;
|
|
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`<div/>`;
|
|
async willStart() {
|
|
ok = true;
|
|
}
|
|
}
|
|
|
|
class ParentWidget extends Component {
|
|
static template = xml`<div><t t-component="child"/></div>`;
|
|
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`<div/>`;
|
|
mounted() {
|
|
expect(document.body.contains(this.el)).toBe(true);
|
|
steps.push("child:mounted");
|
|
}
|
|
}
|
|
|
|
class ParentWidget extends Component {
|
|
static template = xml`<div><ChildWidget /></div>`;
|
|
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`<div/>`;
|
|
mounted() {
|
|
steps.push("childchild:mounted");
|
|
}
|
|
willUnmount() {
|
|
steps.push("childchild:willUnmount");
|
|
}
|
|
}
|
|
|
|
class ChildWidget extends Component {
|
|
static template = xml`<div><t t-component="childchild"/></div>`;
|
|
static components = { childchild: ChildChildWidget };
|
|
mounted() {
|
|
steps.push("child:mounted");
|
|
}
|
|
willUnmount() {
|
|
steps.push("child:willUnmount");
|
|
}
|
|
}
|
|
|
|
class ParentWidget extends Component {
|
|
static template = xml`<div><t t-if="state.flag"><t t-component="child"/></t></div>`;
|
|
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", `<div><t t-component="childchild" n="props.n"/></div>`);
|
|
env.qweb.addTemplate("ChildChildWidget", `<div><t t-esc="props.n"/></div>`);
|
|
|
|
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", `<div><t t-component="child" n="state.n"/></div>`);
|
|
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",
|
|
`
|
|
<div>
|
|
<t t-if="state.ok">
|
|
<t t-component="child"/>
|
|
</t>
|
|
<t t-else="">
|
|
<div/>
|
|
</t>
|
|
</div>`
|
|
);
|
|
class ChildWidget extends Component {
|
|
static template = xml`<div/>`;
|
|
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`<div/>`;
|
|
mounted() {
|
|
const child = new ChildWidget(this);
|
|
child.mount(this.el!);
|
|
}
|
|
}
|
|
class ChildWidget extends Component {
|
|
static template = xml`<div/>`;
|
|
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`<div/>`;
|
|
setup() {
|
|
steps.push("setup");
|
|
}
|
|
async willStart() {
|
|
steps.push("willstart");
|
|
}
|
|
mounted() {
|
|
steps.push("mounted");
|
|
}
|
|
willUnmount() {
|
|
steps.push("willunmount");
|
|
}
|
|
}
|
|
class ParentWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-if="state.ok"><ChildWidget /></t>
|
|
</div>
|
|
`;
|
|
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`<span><t t-esc="props.n"/></span>`;
|
|
willUnmount() {
|
|
childUnmounted = true;
|
|
}
|
|
}
|
|
|
|
class ParentWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<div t-if="state.flag">
|
|
<ChildWidget n="state.n"/>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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("<div><div><span>0</span></div></div>");
|
|
widget.increment();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><div><span>1</span></div></div>");
|
|
widget.toggleSubWidget();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
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`<div/>`;
|
|
setup() {
|
|
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`<div><t t-component="child"/></div>`;
|
|
static components = { child: ChildWidget };
|
|
setup() {
|
|
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", '<span><Child n="state.n"/></span>');
|
|
class HookWidget extends Component {
|
|
static template = xml`<span><t t-esc="props.n"/></span>`;
|
|
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("<span><span>1</span></span>");
|
|
widget.state.n = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<span><span>1</span></span>");
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<span><span>2</span></span>");
|
|
});
|
|
|
|
test("patched hook is called after updating State", async () => {
|
|
let n = 0;
|
|
|
|
class TestWidget extends Component {
|
|
static template = xml`<div/>`;
|
|
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`<div/>`;
|
|
patched() {
|
|
n++;
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child a="state.a"/></div>`;
|
|
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`<div><t t-esc="props.val"/></div>`;
|
|
shouldUpdate() {
|
|
return shouldUpdate;
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child val="state.val"/></div>`;
|
|
static components = { Child: TestWidget };
|
|
state = useState({ val: 42 });
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>42</div></div>");
|
|
widget.state.val = 123;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><div>42</div></div>");
|
|
shouldUpdate = true;
|
|
widget.state.val = 666;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><div>666</div></div>");
|
|
});
|
|
|
|
test("sub widget (inside sub node): hooks are correctly called", async () => {
|
|
let created = false;
|
|
let mounted = false;
|
|
|
|
class ChildWidget extends Component {
|
|
static template = xml`<div/>`;
|
|
constructor(parent, props) {
|
|
super(parent, props);
|
|
created = true;
|
|
}
|
|
mounted() {
|
|
mounted = true;
|
|
}
|
|
}
|
|
class ParentWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-if="state.flag">
|
|
<div><t t-component="child"/></div>
|
|
</t>
|
|
</div>
|
|
`;
|
|
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`<div/>`;
|
|
willPatch() {
|
|
steps.push("child:willPatch");
|
|
}
|
|
patched() {
|
|
steps.push("child:patched");
|
|
}
|
|
}
|
|
class ParentWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-component="child" v="state.n"/>
|
|
</div>
|
|
`;
|
|
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",
|
|
]);
|
|
});
|
|
|
|
test("willPatch/patched hook is not called if not mounted in DOM", async () => {
|
|
const steps: string[] = [];
|
|
|
|
class ChildWidget extends Component {
|
|
static template = xml`<div/>`;
|
|
constructor(parent, props) {
|
|
super(parent, props);
|
|
steps.push("child:constructor");
|
|
}
|
|
mounted() {
|
|
steps.push("child:mounted");
|
|
}
|
|
willPatch() {
|
|
steps.push("child:willPatch");
|
|
}
|
|
patched() {
|
|
steps.push("child:patched");
|
|
}
|
|
}
|
|
class ParentWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-component="child" v="state.n"/>
|
|
</div>
|
|
`;
|
|
static components = { child: ChildWidget };
|
|
state = useState({ n: 1 });
|
|
constructor() {
|
|
super();
|
|
steps.push("parent:constructor");
|
|
}
|
|
mounted() {
|
|
steps.push("parent:mounted");
|
|
}
|
|
willPatch() {
|
|
steps.push("parent:willPatch");
|
|
}
|
|
patched() {
|
|
steps.push("parent:patched");
|
|
}
|
|
}
|
|
|
|
const div = document.createElement("div");
|
|
const widget = new ParentWidget();
|
|
await widget.mount(div);
|
|
expect(steps).toEqual(["parent:constructor", "child:constructor"]);
|
|
|
|
widget.state.n = 2;
|
|
await nextTick();
|
|
|
|
expect(steps).toEqual(["parent:constructor", "child:constructor"]);
|
|
|
|
// then we remount the component in the dom
|
|
await widget.mount(fixture);
|
|
|
|
expect(steps).toEqual([
|
|
"parent:constructor",
|
|
"child:constructor",
|
|
"child:mounted",
|
|
"parent:mounted",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("destroy method", () => {
|
|
test("destroy remove the widget from the DOM", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
|
|
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__.status).toBe(STATUS.DESTROYED);
|
|
});
|
|
|
|
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__.status).toBe(STATUS.MOUNTED);
|
|
parent.destroy();
|
|
expect(child.__owl__.status).toBe(STATUS.DESTROYED);
|
|
});
|
|
|
|
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`<div/>`;
|
|
willStart() {
|
|
return def;
|
|
}
|
|
}
|
|
expect(fixture.innerHTML).toBe("");
|
|
const widget = new DelayedWidget();
|
|
expect(widget.__owl__.status).toBe(STATUS.CREATED);
|
|
widget.mount(fixture);
|
|
expect(widget.__owl__.status).toBe(STATUS.WILLSTARTED);
|
|
widget.destroy();
|
|
expect(widget.__owl__.status).toBe(STATUS.DESTROYED);
|
|
def.resolve();
|
|
await nextTick();
|
|
|
|
expect(widget.__owl__.status).toBe(STATUS.DESTROYED);
|
|
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`
|
|
<span>
|
|
<t t-esc="props.val.val"/>
|
|
</span>
|
|
`;
|
|
}
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<span>
|
|
<GrandChild t-if="state.flag" val="something"/>
|
|
<button t-on-click="doSomething">click</button>
|
|
</span>
|
|
`;
|
|
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`
|
|
<div t-on-some-event="doStuff">
|
|
<Child />
|
|
</div>
|
|
`;
|
|
static components = { Child };
|
|
state = useState({ p: 1 });
|
|
doStuff() {
|
|
this.state.p = 2;
|
|
}
|
|
}
|
|
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span><button>click</button></span></div>");
|
|
fixture.querySelector("button")!.click();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span><span>12</span><button>click</button></span></div>");
|
|
});
|
|
|
|
test("destroying a widget before being mounted (2)", async () => {
|
|
const steps: string[] = [];
|
|
class Child extends Component {
|
|
static template = xml`<span></span>`;
|
|
willStart() {
|
|
steps.push("willStart");
|
|
return makeDeferred();
|
|
}
|
|
__destroy(parent) {
|
|
steps.push("__destroy");
|
|
super.__destroy(parent);
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child t-if="state.flag"/></div>`;
|
|
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("<div></div>");
|
|
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("<div>Hello<div>world</div></div>");
|
|
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", `<div><t t-component="WidgetB"/></div>`);
|
|
class ParentWidget extends Component {}
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>world</div></div>");
|
|
delete QWeb.components["WidgetB"];
|
|
});
|
|
|
|
test("can use dynamic components (the class) if given", async () => {
|
|
class A extends Component {
|
|
static template = xml`<span>child a</span>`;
|
|
}
|
|
class B extends Component {
|
|
static template = xml`<span>child b</span>`;
|
|
}
|
|
class App extends Component {
|
|
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
|
|
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("<span>child a</span>");
|
|
widget.state.child = "b";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<span>child b</span>");
|
|
});
|
|
|
|
test("can use dynamic components (the class) if given (with different root tagname)", async () => {
|
|
class A extends Component {
|
|
static template = xml`<span>child a</span>`;
|
|
}
|
|
class B extends Component {
|
|
static template = xml`<div>child b</div>`;
|
|
}
|
|
class App extends Component {
|
|
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
|
|
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("<span>child a</span>");
|
|
widget.state.child = "b";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>child b</div>");
|
|
});
|
|
|
|
test("can switch between dynamic components without the need for a t-key", async () => {
|
|
class A extends Component {
|
|
static template = xml`<span>child a</span>`;
|
|
}
|
|
class B extends Component {
|
|
static template = xml`<span>child b</span>`;
|
|
}
|
|
class App extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-component="{{state.child}}"/>
|
|
</div>`;
|
|
static components = { A, B };
|
|
|
|
state = useState({ child: "A" });
|
|
}
|
|
const app = await mount(App, { target: fixture });
|
|
expect(fixture.innerHTML).toBe("<div><span>child a</span></div>");
|
|
app.state.child = "B";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>child b</span></div>");
|
|
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", `<div><t t-component="WidgetB"/></div>`);
|
|
env.qweb.addTemplate("AnotherWidgetB", `<span>Belgium</span>`);
|
|
class AnotherWidgetB extends Component {}
|
|
class ParentWidget extends Component {
|
|
static components = { WidgetB: AnotherWidgetB };
|
|
}
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>Belgium</span></div>");
|
|
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", `<div><t t-component="WidgetB"/></div>`);
|
|
env.qweb.addTemplate("ComponentWidgetB", `<span>Belgium</span>`); // should not use this widget either
|
|
env.qweb.addTemplate("InstanceWidgetB", `<span>Chocolate</span>`); // 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("<div><span>Chocolate</span></div>");
|
|
delete QWeb.components["WidgetB"];
|
|
});
|
|
|
|
test("can define components in template without t-component", async () => {
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<div t-name="P"><C a="1"/></div>
|
|
<span t-name="C"><t t-esc="props.a"/></span>
|
|
</templates>`);
|
|
class C extends Component {}
|
|
class P extends Component {
|
|
static components = { C };
|
|
}
|
|
const parent = new P();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
});
|
|
|
|
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", `<div><SomeMispelledWidget /></div>`);
|
|
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`
|
|
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
|
|
state = useState({
|
|
counter: 0,
|
|
});
|
|
}
|
|
|
|
env.qweb.addTemplate("ParentWidget", `<div><t t-component="Counter"/></div>`);
|
|
class ParentWidget extends Component {
|
|
static components = { Counter };
|
|
}
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>0<button>Inc</button></div></div>");
|
|
const button = fixture.getElementsByTagName("button")[0];
|
|
await button.click();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><div>1<button>Inc</button></div></div>");
|
|
});
|
|
|
|
test("refs in a loop", async () => {
|
|
env.qweb.addTemplate(
|
|
"ParentWidget",
|
|
`<div>
|
|
<t t-foreach="state.items" t-as="item">
|
|
<t t-component="Child" t-ref="{{item}}" t-key="item"/>
|
|
</t>
|
|
</div>`
|
|
);
|
|
class SomeComponent extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
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("t-ref on a node, and t-on-click", async () => {
|
|
let c;
|
|
let v = false;
|
|
class Child extends Component {
|
|
static template = xml`<div t-ref="nibor">nibor</div>`;
|
|
nibor = useRef("nibor");
|
|
mounted() {
|
|
c = this;
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child t-on-click="doSomething"/></div>`;
|
|
static components = { Child };
|
|
doSomething() {
|
|
v = true;
|
|
}
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>nibor</div></div>");
|
|
expect(c.nibor.el).toBeInstanceOf(HTMLElement);
|
|
expect(v).toBe(false);
|
|
c.nibor.el.dispatchEvent(new Event("click"));
|
|
expect(v).toBe(true);
|
|
expect(QWeb.TEMPLATES[Child.template].fn.toString()).toMatchSnapshot();
|
|
expect(QWeb.TEMPLATES[Parent.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("parent's elm for a children === children's elm, even after rerender", async () => {
|
|
const widget = new WidgetA();
|
|
await widget.mount(fixture);
|
|
|
|
expect((<any>widget.__owl__.vnode!.children![1]).elm).toBe(
|
|
(<any>children(widget)[0].__owl__.vnode).elm
|
|
);
|
|
await children(widget)[0].render();
|
|
expect((<any>widget.__owl__.vnode!.children![1]).elm).toBe(
|
|
(<any>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`
|
|
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
|
|
state = useState({
|
|
counter: 0,
|
|
});
|
|
}
|
|
|
|
env.qweb.addTemplate("ParentWidget", `<div><t t-component="Counter"/></div>`);
|
|
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("<div><div>1<button>Inc</button></div></div>");
|
|
await widget.render();
|
|
expect(fixture.innerHTML).toBe("<div><div>1<button>Inc</button></div></div>");
|
|
});
|
|
|
|
test("sub components are destroyed if no longer in dom, then recreated", async () => {
|
|
class Counter extends Component {
|
|
static template = xml`
|
|
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
|
|
state = useState({
|
|
counter: 0,
|
|
});
|
|
}
|
|
|
|
env.qweb.addTemplate("ParentWidget", `<div><t t-if="state.ok"><Counter /></t></div>`);
|
|
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("<div><div>1<button>Inc</button></div></div>");
|
|
widget.state.ok = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
|
|
widget.state.ok = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><div>0<button>Inc</button></div></div>");
|
|
});
|
|
|
|
test("sub components rendered in a loop", async () => {
|
|
env.qweb.addTemplate("ChildWidget", `<span><t t-esc="props.n"/></span>`);
|
|
class ChildWidget extends Component {}
|
|
|
|
env.qweb.addTemplate(
|
|
"Parent",
|
|
`
|
|
<div>
|
|
<t t-foreach="state.numbers" t-as="number">
|
|
<t t-component="ChildWidget" t-key="number" n="number"/>
|
|
</t>
|
|
</div>`
|
|
);
|
|
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(`
|
|
<div>
|
|
<span>1</span>
|
|
<span>2</span>
|
|
<span>3</span>
|
|
</div>
|
|
`)
|
|
);
|
|
});
|
|
|
|
test("sub components with some state rendered in a loop", async () => {
|
|
let n = 1;
|
|
env.qweb.addTemplate("ChildWidget", `<span><t t-esc="state.n"/></span>`);
|
|
class ChildWidget extends Component {
|
|
state: any;
|
|
constructor(parent) {
|
|
super(parent);
|
|
this.state = useState({ n });
|
|
n++;
|
|
}
|
|
}
|
|
|
|
env.qweb.addTemplate(
|
|
"parent",
|
|
`<div>
|
|
<t t-foreach="state.numbers" t-as="number">
|
|
<t t-component="ChildWidget" t-key="number"/>
|
|
</t>
|
|
</div>`
|
|
);
|
|
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(`
|
|
<div>
|
|
<span>1</span>
|
|
<span>3</span>
|
|
</div>
|
|
`)
|
|
);
|
|
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`<span>child</span>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<h1 t-if="state.flag">hey</h1>
|
|
<h2 t-else="">noo</h2>
|
|
<span><ChildWidget/></span>
|
|
<t t-if="state.flag"><span>test</span></t>
|
|
</div>`;
|
|
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(`
|
|
<div>
|
|
<h2>noo</h2>
|
|
<span><span>child</span></span>
|
|
</div>
|
|
`)
|
|
);
|
|
|
|
parent.state.flag = true;
|
|
await nextTick();
|
|
expect(children(parent)[0]).toBe(child);
|
|
expect(child.__owl__.status).toBe(STATUS.MOUNTED);
|
|
expect(normalize(fixture.innerHTML)).toBe(
|
|
normalize(`
|
|
<div>
|
|
<h1>hey</h1>
|
|
<span><span>child</span></span>
|
|
<span>test</span>
|
|
</div>
|
|
`)
|
|
);
|
|
});
|
|
|
|
test("list of sub components inside other nodes", async () => {
|
|
// this confuses the patching algorithm...
|
|
env.qweb.addTemplate("ChildWidget", `<span>child</span>`);
|
|
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<div t-name="Parent">
|
|
<div t-foreach="state.blips" t-as="blip" t-key="blip.id">
|
|
<SubWidget />
|
|
</div>
|
|
</div>
|
|
<span t-name="SubWidget">asdf</span>
|
|
</templates>`);
|
|
|
|
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(
|
|
"<div><div><span>asdf</span></div><div><span>asdf</span></div><div><span>asdf</span></div></div>"
|
|
);
|
|
parent.state.blips.splice(0, 1);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div><span>asdf</span></div><div><span>asdf</span></div></div>"
|
|
);
|
|
});
|
|
|
|
test("list of two sub components inside other nodes", async () => {
|
|
// this confuses the patching algorithm...
|
|
env.qweb.addTemplate("ChildWidget", `<span>child</span>`);
|
|
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<div t-name="Parent">
|
|
<div t-foreach="state.blips" t-as="blip" t-key="blip.id">
|
|
<SubWidget />
|
|
<SubWidget />
|
|
</div>
|
|
</div>
|
|
<span t-name="SubWidget">asdf</span>
|
|
</templates>`);
|
|
|
|
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("<div><div><span>asdf</span><span>asdf</span></div></div>");
|
|
});
|
|
|
|
test("t-component with dynamic value", async () => {
|
|
env.qweb.addTemplate("ParentWidget", `<div><t t-component="{{state.widget}}"/></div>`);
|
|
class ParentWidget extends Component {
|
|
static components = { WidgetB };
|
|
state = useState({ widget: "WidgetB" });
|
|
}
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>world</div></div>");
|
|
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-component with dynamic value 2", async () => {
|
|
env.qweb.addTemplate("ParentWidget", `<div><t t-component="Widget{{state.widget}}"/></div>`);
|
|
class ParentWidget extends Component {
|
|
static components = { WidgetB };
|
|
state = useState({ widget: "B" });
|
|
}
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>world</div></div>");
|
|
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-component not on a <t> node", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span>1</span>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`<div><div t-component="Child"/></div>`;
|
|
}
|
|
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 <t> nodes (used on a <div>)`
|
|
);
|
|
});
|
|
|
|
test("sub components, loops, and shouldUpdate", async () => {
|
|
class ChildWidget extends Component {
|
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
|
shouldUpdate(nextProps) {
|
|
if (nextProps.val === 12) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="state.records" t-as="record">
|
|
<ChildWidget t-key="record.id" val="record.val"/>
|
|
</t>
|
|
</div>`;
|
|
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(
|
|
"<div><span>1</span><span>2</span><span>3</span></div>"
|
|
);
|
|
|
|
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(
|
|
"<div><span>11</span><span>2</span><span>13</span></div>"
|
|
);
|
|
});
|
|
|
|
test("three level of components with collapsing root nodes", async () => {
|
|
class GrandChild extends Component {
|
|
static template = xml`<div>2</div>`;
|
|
}
|
|
class Child extends Component {
|
|
static components = { GrandChild };
|
|
static template = xml`<GrandChild/>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`<Child></Child>`;
|
|
}
|
|
|
|
const app = new Parent();
|
|
await app.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div>2</div>");
|
|
});
|
|
});
|
|
|
|
describe("props evaluation ", () => {
|
|
test("explicit object prop", async () => {
|
|
env.qweb.addTemplate("Parent", `<div><t t-component="child" value="state.val"/></div>`);
|
|
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", `<span><t t-esc="state.someval"/></span>`);
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
|
});
|
|
|
|
test("accept ES6-like syntax for props (with getters)", async () => {
|
|
env.qweb.addTemplate("Child", `<span><t t-esc="props.greetings"/></span>`);
|
|
class Child extends Component {}
|
|
|
|
env.qweb.addTemplate("Parent", `<div><t t-component="child" greetings="greetings"/></div>`);
|
|
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("<div><span>hello aaron</span></div>");
|
|
});
|
|
|
|
test("t-set works ", async () => {
|
|
class Child extends Component {}
|
|
env.qweb.addTemplate(
|
|
"Parent",
|
|
`
|
|
<div>
|
|
<t t-set="val" t-value="42"/>
|
|
<t t-component="child" val="val"/>
|
|
</div>`
|
|
);
|
|
class Parent extends Component {
|
|
static components = { child: Child };
|
|
}
|
|
env.qweb.addTemplate(
|
|
"Child",
|
|
`
|
|
<span>
|
|
<t t-esc="props.val"/>
|
|
</span>`
|
|
);
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(normalize(fixture.innerHTML)).toBe("<div><span>42</span></div>");
|
|
});
|
|
|
|
test("t-set with a body expression can be used as textual prop", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span t-esc="props.val"/>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="abc">42</t>
|
|
<Child val="abc"/>
|
|
</div>`;
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
|
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`
|
|
<span>
|
|
<t t-esc="props.val"/>
|
|
<t t-raw="props.val"/>
|
|
</span>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="abc"><p>43</p></t>
|
|
<Child val="abc"/>
|
|
</div>`;
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span><p>4343</p><p>43</p></span></div>");
|
|
});
|
|
|
|
test("arrow function prop captures component instance as 'this'", async () => {
|
|
expect.assertions(5);
|
|
let child, parent;
|
|
class Child extends Component {
|
|
setup() {
|
|
child = this;
|
|
}
|
|
}
|
|
env.qweb.addTemplate("Child", `<span><t t-esc="props.value"/></span>`);
|
|
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
state = useState({ val: 42 });
|
|
setup() {
|
|
parent = this;
|
|
}
|
|
setValue(value) {
|
|
expect(this).toBe(parent);
|
|
this.state.val = value;
|
|
}
|
|
}
|
|
env.qweb.addTemplate(
|
|
"Parent",
|
|
`<div>
|
|
<Child callback="value => this.setValue(value)" value="state.val"/>
|
|
</div>`
|
|
);
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
|
child.props.callback(123);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
|
|
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", `<span><t t-esc="props.value"/></span>`);
|
|
|
|
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",
|
|
`<div>
|
|
<t t-set="ctxVal" t-value="2"/>
|
|
<Child callback="value => setValue(value)" value="state.val"/>
|
|
</div>`
|
|
);
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
|
child.props.callback(123);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
|
|
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
|
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("arrow function prop captures context component instance as 'this' inside slot", async () => {
|
|
expect.assertions(7);
|
|
let child, parent;
|
|
class Child extends Component {
|
|
setup() {
|
|
child = this;
|
|
}
|
|
}
|
|
env.qweb.addTemplate("Child", `<span><t t-esc="props.value"/></span>`);
|
|
|
|
class Wrapper extends Component {}
|
|
env.qweb.addTemplate("Wrapper", `<t t-slot="default"/>`);
|
|
|
|
class Parent extends Component {
|
|
static components = { Child, Wrapper };
|
|
state = useState({ val: 42 });
|
|
setup() {
|
|
parent = this;
|
|
}
|
|
setValue(value) {
|
|
expect(this).toBe(parent);
|
|
this.state.val = value;
|
|
}
|
|
}
|
|
env.qweb.addTemplate(
|
|
"Parent",
|
|
`<div>
|
|
<Wrapper>
|
|
<Child callback="value => this.setValue(value)" value="state.val"/>
|
|
</Wrapper>
|
|
</div>`
|
|
);
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
|
child.props.callback(123);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
|
|
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
|
expect(env.qweb.templates.Wrapper.fn.toString()).toMatchSnapshot();
|
|
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
|
|
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("arrow function prop captures context component instance as 'this' inside slot default content", async () => {
|
|
expect.assertions(6);
|
|
let child, wrapper;
|
|
class Child extends Component {
|
|
setup() {
|
|
child = this;
|
|
}
|
|
}
|
|
env.qweb.addTemplate("Child", `<span><t t-esc="props.value"/></span>`);
|
|
|
|
class Wrapper extends Component {
|
|
static components = { Child };
|
|
state = useState({ val: 42 });
|
|
setup() {
|
|
wrapper = this;
|
|
}
|
|
setValue(value) {
|
|
expect(this).toBe(wrapper);
|
|
this.state.val = value;
|
|
}
|
|
}
|
|
env.qweb.addTemplate(
|
|
"Wrapper",
|
|
`<t t-slot="default">
|
|
<Child callback="value => this.setValue(value)" value="state.val"/>
|
|
</t>`
|
|
);
|
|
|
|
class Parent extends Component {
|
|
static components = { Wrapper };
|
|
}
|
|
env.qweb.addTemplate(
|
|
"Parent",
|
|
`<div>
|
|
<Wrapper/>
|
|
</div>`
|
|
);
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
|
child.props.callback(123);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
|
|
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
|
expect(env.qweb.templates.Wrapper.fn.toString()).toMatchSnapshot();
|
|
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("arrow function prop captures loop variables", async () => {
|
|
let children = [];
|
|
class Child extends Component {
|
|
setup() {
|
|
children.push(this);
|
|
}
|
|
}
|
|
env.qweb.addTemplate("Child", `<span><t t-esc="props.value"/></span>`);
|
|
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
state = useState({ val: 42 });
|
|
setValue(value) {
|
|
this.state.val = value;
|
|
}
|
|
}
|
|
env.qweb.addTemplate(
|
|
"Parent",
|
|
`<div>
|
|
<t t-foreach="[0, 1]" t-as="loopVar" t-key="loopVar">
|
|
<Child callback="() => this.setValue(loopVar)" value="state.val"/>
|
|
</t>
|
|
</div>`
|
|
);
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>42</span><span>42</span></div>");
|
|
children[0].props.callback();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>0</span><span>0</span></div>");
|
|
children[1].props.callback();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span><span>1</span></div>");
|
|
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
|
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
describe("other directives with t-component", () => {
|
|
test("t-on works as expected", async () => {
|
|
expect.assertions(4);
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<div t-name="ParentWidget"><t t-component="child" t-on-custom-event="someMethod"/></div>
|
|
</templates>
|
|
`);
|
|
class Child extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
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`<span>child</span>`;
|
|
mounted() {
|
|
mounted = true;
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child t-on-click="doSomething"/></div>`;
|
|
static components = { Child };
|
|
|
|
doSomething() {
|
|
flag = true;
|
|
}
|
|
}
|
|
|
|
const parent = new Parent();
|
|
parent.mount(fixture);
|
|
while (!mounted) {
|
|
await nextMicroTick();
|
|
Component.scheduler.flush();
|
|
}
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>child</span></div>");
|
|
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`<span>child</span>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child t-on-click="state.n = state.n + 1"/></div>`;
|
|
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(`
|
|
<templates>
|
|
<div t-name="ParentWidget"><t t-component="child" t-on-ev="onEv(3)"/></div>
|
|
</templates>
|
|
`);
|
|
class Child extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
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(`
|
|
<templates>
|
|
<div t-name="ParentWidget"><t t-component="child" t-on-ev="onEv({val: 3})"/></div>
|
|
</templates>
|
|
`);
|
|
class Child extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
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(`
|
|
<templates>
|
|
<div t-name="ParentWidget"><t t-component="child" t-on-ev="onEv({})"/></div>
|
|
</templates>
|
|
`);
|
|
class Child extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
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(`
|
|
<templates>
|
|
<div t-name="ParentWidget"><t t-component="child" t-on-ev="onEv({ })"/></div>
|
|
</templates>
|
|
`);
|
|
class Child extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
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(`
|
|
<templates>
|
|
<div t-name="ParentWidget">
|
|
<t t-component="child"
|
|
t-on-ev-1.stop="onEv1"
|
|
t-on-ev-2.prevent="onEv2"
|
|
t-on-ev-3.stop.prevent="onEv3"/>
|
|
</div>
|
|
</templates>
|
|
`);
|
|
class Child extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
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(`
|
|
<templates>
|
|
<div t-name="ParentWidget">
|
|
<t t-component="child" t-on-ev-1="onEv1" t-on-ev-2.self="onEv2"/>
|
|
</div>
|
|
<div t-name="Child"><t t-component="child"/></div>
|
|
</templates>
|
|
`);
|
|
const steps: string[] = [];
|
|
class GrandChild extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
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(`
|
|
<templates>
|
|
<div t-name="ParentWidget">
|
|
<t t-component="child" t-on-ev.self.prevent="onEv"/>
|
|
</div>
|
|
<div t-name="Child"><t t-component="child"/></div>
|
|
</templates>
|
|
`);
|
|
const steps: boolean[] = [];
|
|
class GrandChild extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
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);
|
|
(<HTMLElement>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(`
|
|
<templates>
|
|
<div t-name="ParentWidget">
|
|
<Child t-on-ev.prevent.self="onEv"/>
|
|
</div>
|
|
<div t-name="Child"><GrandChild/></div>
|
|
</templates>
|
|
`);
|
|
const steps: boolean[] = [];
|
|
class GrandChild extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
|
|
class Child extends Component {
|
|
static components = { GrandChild };
|
|
}
|
|
class ParentWidget extends Component {
|
|
static components = { Child };
|
|
onEv() {}
|
|
}
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
(<HTMLElement>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`<span></span>`;
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-esc="state.counter"/>
|
|
<Child t-on-ev="handler"/>
|
|
</div>`;
|
|
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("<div>0<span></span></div>");
|
|
|
|
let child = children(parent)[0];
|
|
child.trigger("ev");
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>1<span></span></div>");
|
|
});
|
|
|
|
test("t-on with inline statement", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span></span>`;
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-esc="state.counter"/>
|
|
<Child t-on-ev="state.counter++"/>
|
|
</div>`;
|
|
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("<div>0<span></span></div>");
|
|
|
|
let child = children(parent)[0];
|
|
child.trigger("ev");
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>1<span></span></div>");
|
|
});
|
|
|
|
test("t-on with no handler (only modifiers)", async () => {
|
|
expect.assertions(2);
|
|
class ComponentB extends Component {
|
|
static template = xml`<span></span>`;
|
|
}
|
|
class ComponentA extends Component {
|
|
static template = xml`<p><ComponentB t-on-ev.prevent=""/></p>`;
|
|
static components = { ComponentB };
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><ComponentA t-on-ev="onEv"/></div>`;
|
|
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`<span t-on-ev="_onEv"/>`;
|
|
constructor() {
|
|
super(...arguments);
|
|
grandChild = this;
|
|
}
|
|
_onEv() {
|
|
steps.push("GrandChild");
|
|
}
|
|
}
|
|
class Child extends Component {
|
|
static template = xml`<GrandChild t-on-ev="_onEv"/>`;
|
|
static components = { GrandChild };
|
|
_onEv() {
|
|
steps.push("Child");
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<Child t-on-ev="_onEv"/>`;
|
|
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`<div t-on-click="onClick"/>`;
|
|
mounted() {
|
|
child = this;
|
|
}
|
|
onClick() {
|
|
steps.push("click");
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child/></div>`;
|
|
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__.status).toBe(STATUS.UNMOUNTED);
|
|
el.click();
|
|
expect(steps).toEqual(["click", "click"]);
|
|
});
|
|
|
|
test("triggering custom event on mounted components", async () => {
|
|
let value = false;
|
|
class Child extends Component {
|
|
static template = xml`<div/>`;
|
|
mounted() {
|
|
this.trigger("coucou");
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<Child t-on-coucou="doSomething"/>`;
|
|
static components = { Child };
|
|
doSomething() {
|
|
value = true;
|
|
}
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(value).toBe(true);
|
|
});
|
|
|
|
test("t-on with .capture modifier", async () => {
|
|
const steps: string[] = [];
|
|
|
|
class Child extends Component {
|
|
static template = xml`<div><span t-on-click="click">hey</span></div>`;
|
|
click() {
|
|
steps.push("click");
|
|
}
|
|
}
|
|
class ParentWidget extends Component {
|
|
static components = { Child };
|
|
static template = xml`<div><Child t-on-click.capture="capture"/></div>`;
|
|
capture() {
|
|
steps.push("captured");
|
|
}
|
|
}
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
|
|
fixture.querySelector("span")!.click();
|
|
expect(env.qweb.templates[ParentWidget.template].fn.toString()).toMatchSnapshot();
|
|
expect(steps).toEqual(["captured", "click"]);
|
|
});
|
|
|
|
test("t-on on destroyed components", async () => {
|
|
const steps: string[] = [];
|
|
let child;
|
|
class Child extends Component {
|
|
static template = xml`<div t-on-click="onClick"/>`;
|
|
mounted() {
|
|
child = this;
|
|
}
|
|
onClick() {
|
|
steps.push("click");
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child t-if="state.flag"/></div>`;
|
|
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__.status).toBe(STATUS.DESTROYED);
|
|
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`<div>grandchild</div>`;
|
|
}
|
|
class Child extends Component {
|
|
static template = xml`<div><GrandChild t-ref="gc" t-on-click="onClick"/></div>`;
|
|
static components = { GrandChild };
|
|
gc = useRef("gc");
|
|
mounted() {
|
|
child = this;
|
|
}
|
|
onClick() {
|
|
steps.push("click");
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child t-if="state.flag"/></div>`;
|
|
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__.status).toBe(STATUS.DESTROYED);
|
|
el.click();
|
|
expect(steps).toEqual(["click"]);
|
|
});
|
|
|
|
test("t-if works with t-component", async () => {
|
|
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child" t-if="state.flag"/></div>`);
|
|
class Child extends Component {}
|
|
class ParentWidget extends Component {
|
|
static components = { child: Child };
|
|
state = useState({ flag: true });
|
|
}
|
|
env.qweb.addTemplate("Child", "<span>hey</span>");
|
|
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
|
|
|
|
widget.state.flag = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
|
|
widget.state.flag = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
|
|
});
|
|
|
|
test("t-else works with t-component", async () => {
|
|
env.qweb.addTemplate(
|
|
"ParentWidget",
|
|
`
|
|
<div>
|
|
<div t-if="state.flag">somediv</div>
|
|
<t t-else="" t-component="child"/>
|
|
</div>`
|
|
);
|
|
class Child extends Component {}
|
|
class ParentWidget extends Component {
|
|
static components = { child: Child };
|
|
state = useState({ flag: true });
|
|
}
|
|
env.qweb.addTemplate("Child", "<span>hey</span>");
|
|
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(normalize(fixture.innerHTML)).toBe("<div><div>somediv</div></div>");
|
|
|
|
widget.state.flag = false;
|
|
await nextTick();
|
|
expect(normalize(fixture.innerHTML)).toBe("<div><span>hey</span></div>");
|
|
});
|
|
|
|
test("t-elif works with t-component", async () => {
|
|
env.qweb.addTemplate(
|
|
"ParentWidget",
|
|
`
|
|
<div>
|
|
<div t-if="state.flag">somediv</div>
|
|
<t t-elif="!state.flag" t-component="child"/>
|
|
</div>`
|
|
);
|
|
class Child extends Component {}
|
|
class ParentWidget extends Component {
|
|
static components = { child: Child };
|
|
state = useState({ flag: true });
|
|
}
|
|
env.qweb.addTemplate("Child", "<span>hey</span>");
|
|
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(normalize(fixture.innerHTML)).toBe("<div><div>somediv</div></div>");
|
|
|
|
widget.state.flag = false;
|
|
await nextTick();
|
|
expect(normalize(fixture.innerHTML)).toBe("<div><span>hey</span></div>");
|
|
});
|
|
|
|
test("t-else with empty string works with t-component", async () => {
|
|
env.qweb.addTemplate(
|
|
"ParentWidget",
|
|
`
|
|
<div>
|
|
<div t-if="state.flag">somediv</div>
|
|
<t t-else="" t-component="child"/>
|
|
</div>`
|
|
);
|
|
class Child extends Component {}
|
|
class ParentWidget extends Component {
|
|
static components = { child: Child };
|
|
state = useState({ flag: true });
|
|
}
|
|
env.qweb.addTemplate("Child", "<span>hey</span>");
|
|
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(normalize(fixture.innerHTML)).toBe("<div><div>somediv</div></div>");
|
|
|
|
widget.state.flag = false;
|
|
await nextTick();
|
|
expect(normalize(fixture.innerHTML)).toBe("<div><span>hey</span></div>");
|
|
});
|
|
|
|
test("t-foreach with t-component, and update", async () => {
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<span>
|
|
<t t-esc="state.val"/>
|
|
<t t-esc="props.val"/>
|
|
</span>`;
|
|
state = useState({ val: "A" });
|
|
mounted() {
|
|
this.state.val = "B";
|
|
}
|
|
}
|
|
class ParentWidget extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="Array(2)" t-as="n" t-key="n_index">
|
|
<Child val="n_index"/>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
|
|
const widget = new ParentWidget();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>A0</span><span>A1</span></div>");
|
|
|
|
await nextTick(); // wait for changes triggered in mounted to be applied
|
|
expect(fixture.innerHTML).toBe("<div><span>B0</span><span>B1</span></div>");
|
|
});
|
|
|
|
test("t-set outside modified in t-foreach", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="iter" t-value="0"/>
|
|
<t t-foreach="state.values" t-as="val" t-key="val">
|
|
<p>InLoop: <t t-esc="iter"/></p>
|
|
<t t-set="iter" t-value="iter + 1"/>
|
|
</t>
|
|
<p>EndLoop: <t t-esc="iter"/></p>
|
|
</div>`;
|
|
|
|
state = useState({ values: ["a", "b"] });
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>InLoop: 0</p><p>InLoop: 1</p><p>EndLoop: 2</p></div>");
|
|
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-set outside modified in t-if", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="iter" t-value="0"/>
|
|
<t t-set="flag" t-value="state.flag" />
|
|
<t t-if="flag === 'if'">
|
|
<t t-set="iter" t-value="2"/>
|
|
</t>
|
|
<t t-elif="flag === 'elif'">
|
|
<t t-set="iter" t-value="3"/>
|
|
</t>
|
|
<t t-else="">
|
|
<t t-set="iter" t-value="4"/>
|
|
</t>
|
|
<p><t t-esc="iter"/></p>
|
|
</div>`;
|
|
|
|
state = { flag: "if" };
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
|
|
widget.state.flag = "elif";
|
|
await widget.render();
|
|
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
|
|
widget.state.flag = "false";
|
|
await widget.render();
|
|
expect(fixture.innerHTML).toBe("<div><p>4</p></div>");
|
|
});
|
|
|
|
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`
|
|
<div>
|
|
<t t-set="flag" t-value="state.flag" />
|
|
<t t-if="flag === 'if'">
|
|
<t t-set="iter" t-value="2"/>
|
|
</t>
|
|
<t t-elif="flag === 'elif'">
|
|
<t t-set="iter" t-value="3"/>
|
|
</t>
|
|
<t t-else="">
|
|
<t t-set="iter" t-value="4"/>
|
|
</t>
|
|
<p><t t-esc="iter"/></p>
|
|
</div>`;
|
|
|
|
state = { flag: "if" };
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
|
|
widget.state.flag = "elif";
|
|
await widget.render();
|
|
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
|
|
widget.state.flag = "false";
|
|
await widget.render();
|
|
expect(fixture.innerHTML).toBe("<div><p>4</p></div>");
|
|
});
|
|
|
|
test("t-set can't alter component", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<p><t t-esc="iter"/></p>
|
|
<t t-set="iter" t-value="5"/>
|
|
<p><t t-esc="iter"/></p>
|
|
</div>`;
|
|
|
|
iter = 1;
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>1</p><p>5</p></div>");
|
|
expect(widget.iter).toBe(1);
|
|
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-set can't alter from within callee", async () => {
|
|
env.qweb.addTemplate(
|
|
"ChildWidget",
|
|
`<div><t t-esc="iter"/><t t-set="iter" t-value="'called'"/><t t-esc="iter"/></div>`
|
|
);
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="iter" t-value="'source'"/>
|
|
<p><t t-esc="iter"/></p>
|
|
<t t-call="ChildWidget"/>
|
|
<p><t t-esc="iter"/></p>
|
|
</div>`;
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>source</p><div>sourcecalled</div><p>source</p></div>");
|
|
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-set can't alter in t-call body", async () => {
|
|
env.qweb.addTemplate(
|
|
"ChildWidget",
|
|
`<div><t t-esc="iter"/><t t-set="iter" t-value="'called'"/><t t-esc="iter"/></div>`
|
|
);
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="iter" t-value="'source'"/>
|
|
<p><t t-esc="iter"/></p>
|
|
<t t-call="ChildWidget">
|
|
<t t-set="iter" t-value="'inCall'"/>
|
|
</t>
|
|
<p><t t-esc="iter"/></p>
|
|
</div>`;
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>source</p><div>inCallcalled</div><p>source</p></div>");
|
|
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("slot setted value (with t-set) not accessible with t-esc", async () => {
|
|
class ChildWidget extends Component {
|
|
static template = xml`<div><t t-esc="iter"/><t t-set="iter" t-value="'called'"/><t t-esc="iter"/></div>`;
|
|
}
|
|
class SomeWidget extends Component {
|
|
static components = { ChildWidget };
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="iter" t-value="'source'"/>
|
|
<p><t t-esc="iter"/></p>
|
|
<ChildWidget>
|
|
<t t-set="iter" t-value="'inCall'"/>
|
|
</ChildWidget>
|
|
<p><t t-esc="iter"/></p>
|
|
</div>`;
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>source</p><div>called</div><p>source</p></div>");
|
|
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-set not altered by child widget", async () => {
|
|
let child;
|
|
class ChildWidget extends Component {
|
|
static template = xml`<div><t t-esc="iter"/><t t-set="iter" t-value="'called'"/><t t-esc="iter"/></div>`;
|
|
iter = "child";
|
|
constructor() {
|
|
super(...arguments);
|
|
child = this;
|
|
}
|
|
}
|
|
class SomeWidget extends Component {
|
|
static components = { ChildWidget };
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="iter" t-value="'source'"/>
|
|
<p><t t-esc="iter"/></p>
|
|
<ChildWidget/>
|
|
<p><t t-esc="iter"/></p>
|
|
</div>`;
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>source</p><div>childcalled</div><p>source</p></div>");
|
|
expect(child.iter).toBe("child");
|
|
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-set outside modified in t-foreach increment-after operator", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="iter" t-value="0"/>
|
|
<t t-foreach="state.values" t-as="val" t-key="val">
|
|
<p>InLoop: <t t-esc="iter"/></p>
|
|
<t t-set="iter" t-value="iter++"/>
|
|
</t>
|
|
<p>EndLoop: <t t-esc="iter"/></p>
|
|
</div>`;
|
|
|
|
state = useState({ values: ["a", "b"] });
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>InLoop: 0</p><p>InLoop: 1</p><p>EndLoop: 0</p></div>");
|
|
});
|
|
|
|
test("t-set outside modified in t-foreach increment-before operator", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="iter" t-value="0"/>
|
|
<t t-foreach="state.values" t-as="val" t-key="val">
|
|
<p>InLoop: <t t-esc="iter"/></p>
|
|
<t t-set="iter" t-value="++iter"/>
|
|
</t>
|
|
<p>EndLoop: <t t-esc="iter"/></p>
|
|
</div>`;
|
|
|
|
state = useState({ values: ["a", "b"] });
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>InLoop: 0</p><p>InLoop: 1</p><p>EndLoop: 1</p></div>");
|
|
});
|
|
|
|
test("t-on expression in t-foreach", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<div t-foreach="state.values" t-as="val" t-key="val">
|
|
<t t-esc="val_index"/>: <t t-esc="val + ''"/>
|
|
<button t-on-click="otherState.vals.push(val)">Expr</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
state = useState({ values: ["a", "b"] });
|
|
otherState = { vals: [] };
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div>0: a<button>Expr</button></div><div>1: b<button>Expr</button></div></div>"
|
|
);
|
|
expect(widget.otherState.vals).toStrictEqual([]);
|
|
const buttons = fixture.querySelectorAll("button");
|
|
buttons[0].click();
|
|
buttons[1].click();
|
|
expect(widget.otherState.vals).toStrictEqual(["a", "b"]);
|
|
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-on expression in t-foreach with t-set", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="bossa" t-value="'nova'"/>
|
|
<div t-foreach="state.values" t-as="val" t-key="val">
|
|
<t t-set="bossa" t-value="bossa + '_' + val_index" />
|
|
<t t-esc="val_index"/>: <t t-esc="val + ''"/>
|
|
<button t-on-click="otherState.vals.push(val + '_' + bossa)">Expr</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
state = useState({ values: ["a", "b"] });
|
|
otherState = { vals: [] };
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div>0: a<button>Expr</button></div><div>1: b<button>Expr</button></div></div>"
|
|
);
|
|
expect(widget.otherState.vals).toStrictEqual([]);
|
|
const buttons = fixture.querySelectorAll("button");
|
|
buttons[0].click();
|
|
buttons[1].click();
|
|
expect(widget.otherState.vals).toStrictEqual(["a_nova_0", "b_nova_0_1"]);
|
|
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-on method call in t-foreach", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<div t-foreach="state.values" t-as="val" t-key="val">
|
|
<t t-esc="val_index"/>: <t t-esc="val + ''"/>
|
|
<button t-on-click="addVal(val)">meth call</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
state = useState({ values: ["a", "b"] });
|
|
otherState = { vals: new Array<string>() };
|
|
|
|
addVal(val: string) {
|
|
this.otherState.vals.push(val);
|
|
}
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div>0: a<button>meth call</button></div><div>1: b<button>meth call</button></div></div>"
|
|
);
|
|
expect(widget.otherState.vals).toStrictEqual([]);
|
|
const buttons = fixture.querySelectorAll("button");
|
|
buttons[0].click();
|
|
buttons[1].click();
|
|
expect(widget.otherState.vals).toStrictEqual(["a", "b"]);
|
|
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-on expression captured in t-foreach", async () => {
|
|
class SomeWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="iter" t-value="0" />
|
|
<div t-foreach="arr" t-as="val" t-key="val">
|
|
<button t-on-click="otherState.vals.push(iter + '_' + iter)">expr</button>
|
|
<t t-set="iter" t-value="iter + 1" />
|
|
</div>
|
|
</div>`;
|
|
|
|
arr = ["a", "b"];
|
|
otherState = { vals: new Array<string>() };
|
|
}
|
|
const widget = new SomeWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div><button>expr</button></div><div><button>expr</button></div></div>"
|
|
);
|
|
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",
|
|
`<div><t t-foreach="Array(2)">txt</t><t t-component="widget"/></div>`
|
|
);
|
|
|
|
class SomeComponent extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
|
|
class Test extends Component {
|
|
static components = { widget: SomeComponent };
|
|
}
|
|
const widget = new Test();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>txttxt<div></div></div>");
|
|
});
|
|
|
|
test("t-on with handler bound to dynamic argument on a t-foreach", async () => {
|
|
expect.assertions(3);
|
|
|
|
class Child extends Component {
|
|
static template = xml`<div/>`;
|
|
}
|
|
class ParentWidget extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="items" t-as="item">
|
|
<Child t-key="item" t-on-ev="onEv(item)"/>
|
|
</t>
|
|
</div>`;
|
|
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", `<div><t t-component="child" flag="state.flag"/></div>`);
|
|
class Child extends Component {}
|
|
class Parent extends Component {
|
|
static components = { child: Child };
|
|
state = useState({ flag: false });
|
|
}
|
|
|
|
env.qweb.addTemplate("Child", `<span>abc<t t-if="props.flag">def</t></span>`);
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>abc</span></div>");
|
|
widget.state.flag = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>abcdef</span></div>");
|
|
});
|
|
|
|
test("snapshotting compiled code", async () => {
|
|
env.qweb.addTemplate(
|
|
"Parent",
|
|
`<div><t t-component="child" t-key="'somestring'" flag="state.flag"/></div>`
|
|
);
|
|
class Child extends Component {}
|
|
class Parent extends Component {
|
|
static components = { child: Child };
|
|
state = useState({ flag: false });
|
|
}
|
|
|
|
env.qweb.addTemplate("Child", `<span>abc<t t-if="props.flag">def</t></span>`);
|
|
|
|
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(target, vnode) {
|
|
steps.push(`${this.name}:__patch`);
|
|
super.__patch(target, 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", `<div>B</div>`);
|
|
class B extends TestWidget {
|
|
name = "B";
|
|
constructor(parent, props) {
|
|
super(parent, props);
|
|
steps.push("B:constructor");
|
|
}
|
|
}
|
|
env.qweb.addTemplate("D", `<div>D</div>`);
|
|
class D extends TestWidget {
|
|
name = "D";
|
|
constructor(parent, props) {
|
|
super(parent, props);
|
|
steps.push("D:constructor");
|
|
}
|
|
}
|
|
env.qweb.addTemplate("E", `<div>E</div>`);
|
|
class E extends TestWidget {
|
|
name = "E";
|
|
constructor(parent, props) {
|
|
super(parent, props);
|
|
steps.push("E:constructor");
|
|
}
|
|
}
|
|
|
|
env.qweb.addTemplate("F", `<div>F</div>`);
|
|
class F extends TestWidget {
|
|
name = "F";
|
|
constructor(parent, props) {
|
|
super(parent, props);
|
|
steps.push("F:constructor");
|
|
}
|
|
}
|
|
env.qweb.addTemplate(
|
|
"C",
|
|
`
|
|
<div>C<D />
|
|
<E t-if="state.flag" />
|
|
<F t-else="!state.flag" />
|
|
</div>`
|
|
);
|
|
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", `<div>A<B /><C /></div>`);
|
|
class A extends TestWidget {
|
|
static components = { B, C };
|
|
name = "A";
|
|
}
|
|
|
|
const a = new A();
|
|
await a.mount(fixture);
|
|
expect(fixture.innerHTML).toBe(`<div>A<div>B</div><div>C<div>D</div><div>E</div></div></div>`);
|
|
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`<span><t t-esc="state.n"/></span>`;
|
|
class Parent extends Component {
|
|
static template = xml`<div><t t-call="${SUBTEMPLATE}"/></div>`;
|
|
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("<div><span>42</span></div>");
|
|
});
|
|
|
|
test("update props of component without concrete own node", async () => {
|
|
class Custom extends Component {
|
|
static template = xml`
|
|
<div class="widget-subkey">
|
|
<t t-esc="props.key"/>__<t t-esc="props.subKey"/>
|
|
</div>`;
|
|
}
|
|
class Child extends Component {
|
|
static components = { Custom };
|
|
static template = xml`
|
|
<t t-component="Custom"
|
|
t-key="props.subKey"
|
|
key="props.key"
|
|
subKey="props.subKey"/>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<Child t-key="childProps.key" t-props="childProps"/>
|
|
</div>`;
|
|
childProps = {
|
|
key: 1,
|
|
subKey: 1,
|
|
};
|
|
}
|
|
const parent = new Parent(null);
|
|
await parent.mount(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("two renderings initiated between willPatch and patched", async () => {
|
|
let app;
|
|
|
|
class Panel extends Component {
|
|
static template = xml`<abc><t t-esc="props.val"/></abc>`;
|
|
mounted() {
|
|
app.render();
|
|
}
|
|
willUnmount() {
|
|
app.render();
|
|
}
|
|
}
|
|
|
|
// Main root component
|
|
class App extends Component {
|
|
static components = { Panel };
|
|
static template = xml`<div><Panel t-key="'panel_' + state.panel" val="state.panel"/></div>`;
|
|
|
|
state = useState({ panel: "Panel1" });
|
|
}
|
|
|
|
app = new App();
|
|
await app.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><abc>Panel1</abc></div>");
|
|
app.state.panel = "Panel2";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><abc>Panel2</abc></div>");
|
|
});
|
|
});
|
|
|
|
describe("widget and observable state", () => {
|
|
test("widget is rerendered when its state is changed", async () => {
|
|
class TestWidget extends Component {
|
|
static template = xml`<div><t t-esc="state.drink"/></div>`;
|
|
state = useState({ drink: "water" });
|
|
}
|
|
const widget = new TestWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div>water</div>");
|
|
widget.state.drink = "beer";
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>beer</div>");
|
|
});
|
|
|
|
test("subcomponents cannot change observable state received from parent", async () => {
|
|
const consoleError = console.error;
|
|
console.error = jest.fn();
|
|
env.qweb.addTemplate("Parent", `<div><Child obj="state.obj"/></div>`);
|
|
class Child extends Component {
|
|
static template = xml`<div/>`;
|
|
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", "<span>Orval</span>");
|
|
const abc = new ABC();
|
|
await abc.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<span>Orval</span>");
|
|
});
|
|
|
|
test("can find template of parent component", async () => {
|
|
class ABC extends Component {}
|
|
class DEF extends ABC {}
|
|
env.qweb.addTemplate("ABC", "<span>Orval</span>");
|
|
const def = new DEF();
|
|
await def.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<span>Orval</span>");
|
|
});
|
|
|
|
test("can find template of anonymous component", async () => {
|
|
class ABC extends Component {}
|
|
const Anonymous = class extends ABC {};
|
|
env.qweb.addTemplate("ABC", "<span>Orval</span>");
|
|
const def = new Anonymous();
|
|
await def.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<span>Orval</span>");
|
|
});
|
|
|
|
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", "<span>Orval</span>");
|
|
const def = new DEF();
|
|
await def.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<span>Orval</span>");
|
|
});
|
|
|
|
test("templates are found in proper qweb instance", async () => {
|
|
const env2 = makeTestEnv();
|
|
env.qweb.addTemplate("ABC", "<span>Rochefort 8</span>");
|
|
env2.qweb.addTemplate("ABC", "<span>Rochefort 10</span>");
|
|
class ABC extends Component {}
|
|
const abc = new ABC();
|
|
await abc.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<span>Rochefort 8</span>");
|
|
abc.destroy();
|
|
Component.env = env2;
|
|
const abc2 = new ABC();
|
|
await abc2.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<span>Rochefort 10</span>");
|
|
});
|
|
});
|
|
|
|
describe("t-model directive", () => {
|
|
test("basic use, on an input", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<input t-model="state.text"/>
|
|
<span><t t-esc="state.text"/></span>
|
|
</div>`;
|
|
state = useState({ text: "" });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
|
|
|
const input = fixture.querySelector("input")!;
|
|
await editInput(input, "test");
|
|
expect(comp.state.text).toBe("test");
|
|
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
|
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("basic use, on an input with bracket expression", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<input t-model="state['text']"/>
|
|
<span><t t-esc="state.text"/></span>
|
|
</div>`;
|
|
state = useState({ text: "" });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
|
|
|
const input = fixture.querySelector("input")!;
|
|
await editInput(input, "test");
|
|
expect(comp.state.text).toBe("test");
|
|
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
|
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("throws if invalid expression", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<input t-model="state"/>
|
|
</div>`;
|
|
state = useState({ text: "" });
|
|
}
|
|
const comp = new SomeComponent();
|
|
let error;
|
|
try {
|
|
await comp.mount(fixture);
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
expect(error).toBeDefined();
|
|
expect(error.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
|
|
});
|
|
|
|
test("basic use, on another key in component", async () => {
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<div t-name="SomeComponent">
|
|
<input t-model="some.text"/>
|
|
<span><t t-esc="some.text"/></span>
|
|
</div>
|
|
</templates>`);
|
|
class SomeComponent extends Component {
|
|
some = useState({ text: "" });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
|
|
|
const input = fixture.querySelector("input")!;
|
|
await editInput(input, "test");
|
|
expect(comp.some.text).toBe("test");
|
|
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
|
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("on an input, type=checkbox", async () => {
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<div t-name="SomeComponent">
|
|
<input type="checkbox" t-model="state.flag"/>
|
|
<span>
|
|
<t t-if="state.flag">yes</t>
|
|
<t t-else="">no</t>
|
|
</span>
|
|
</div>
|
|
</templates>`);
|
|
class SomeComponent extends Component {
|
|
state = useState({ flag: false });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe('<div><input type="checkbox"><span>no</span></div>');
|
|
|
|
let input = fixture.querySelector("input")!;
|
|
input.click();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div><input type="checkbox"><span>yes</span></div>');
|
|
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(`
|
|
<templates>
|
|
<div t-name="SomeComponent">
|
|
<textarea t-model="state.text"/>
|
|
<span><t t-esc="state.text"/></span>
|
|
</div>
|
|
</templates>`);
|
|
class SomeComponent extends Component {
|
|
state = useState({ text: "" });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><textarea></textarea><span></span></div>");
|
|
|
|
const textarea = fixture.querySelector("textarea")!;
|
|
await editInput(textarea, "test");
|
|
expect(comp.state.text).toBe("test");
|
|
expect(fixture.innerHTML).toBe("<div><textarea></textarea><span>test</span></div>");
|
|
});
|
|
|
|
test("on an input type=radio", async () => {
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<div t-name="SomeComponent">
|
|
<input type="radio" id="one" value="One" t-model="state.choice"/>
|
|
<input type="radio" id="two" value="Two" t-model="state.choice"/>
|
|
<span>Choice: <t t-esc="state.choice"/></span>
|
|
</div>
|
|
</templates>`);
|
|
class SomeComponent extends Component {
|
|
state = useState({ choice: "" });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: </span></div>'
|
|
);
|
|
|
|
const firstInput = fixture.querySelector("input")!;
|
|
firstInput.click();
|
|
await nextTick();
|
|
expect(comp.state.choice).toBe("One");
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: One</span></div>'
|
|
);
|
|
|
|
const secondInput = fixture.querySelectorAll("input")[1];
|
|
secondInput.click();
|
|
await nextTick();
|
|
expect(comp.state.choice).toBe("Two");
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: Two</span></div>'
|
|
);
|
|
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("on a select", async () => {
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<div t-name="SomeComponent">
|
|
<select t-model="state.color">
|
|
<option value="">Please select one</option>
|
|
<option value="red">Red</option>
|
|
<option value="blue">Blue</option>
|
|
</select>
|
|
<span>Choice: <t t-esc="state.color"/></span>
|
|
</div>
|
|
</templates>`);
|
|
class SomeComponent extends Component {
|
|
state = useState({ color: "" });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div><select><option value="">Please select one</option><option value="red">Red</option><option value="blue">Blue</option></select><span>Choice: </span></div>'
|
|
);
|
|
|
|
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(
|
|
'<div><select><option value="">Please select one</option><option value="red">Red</option><option value="blue">Blue</option></select><span>Choice: red</span></div>'
|
|
);
|
|
|
|
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("on a select, initial state", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<select t-model="state.color">
|
|
<option value="">Please select one</option>
|
|
<option value="red">Red</option>
|
|
<option value="blue">Blue</option>
|
|
</select>
|
|
</div>
|
|
`;
|
|
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`
|
|
<div>
|
|
<input t-model="state.something.text"/>
|
|
<span><t t-esc="state.something.text"/></span>
|
|
</div>
|
|
`;
|
|
state = useState({ something: { text: "" } });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
|
|
|
const input = fixture.querySelector("input")!;
|
|
await editInput(input, "test");
|
|
expect(comp.state.something.text).toBe("test");
|
|
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
|
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test(".lazy modifier", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<input t-model.lazy="state.text"/>
|
|
<span><t t-esc="state.text"/></span>
|
|
</div>
|
|
`;
|
|
state = useState({ text: "" });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
|
|
|
const input = fixture.querySelector("input")!;
|
|
input.value = "test";
|
|
input.dispatchEvent(new Event("input"));
|
|
await nextTick();
|
|
expect(comp.state.text).toBe("");
|
|
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
|
input.dispatchEvent(new Event("change"));
|
|
await nextTick();
|
|
expect(comp.state.text).toBe("test");
|
|
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
|
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test(".trim modifier", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`
|
|
<div t-name="SomeComponent">
|
|
<input t-model.trim="state.text"/>
|
|
<span><t t-esc="state.text"/></span>
|
|
</div>
|
|
`;
|
|
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("<div><input><span>test</span></div>");
|
|
});
|
|
|
|
test(".number modifier", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<input t-model.number="state.number"/>
|
|
<span><t t-esc="state.number"/></span>
|
|
</div>
|
|
`;
|
|
state = useState({ number: 0 });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><input><span>0</span></div>");
|
|
|
|
const input = fixture.querySelector("input")!;
|
|
await editInput(input, "13");
|
|
expect(comp.state.number).toBe(13);
|
|
expect(fixture.innerHTML).toBe("<div><input><span>13</span></div>");
|
|
|
|
await editInput(input, "invalid");
|
|
expect(comp.state.number).toBe("invalid");
|
|
expect(fixture.innerHTML).toBe("<div><input><span>invalid</span></div>");
|
|
});
|
|
|
|
test("in a t-foreach", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="state" t-as="thing" t-key="thing.id">
|
|
<input type="checkbox" t-model="thing.f"/>
|
|
</t>
|
|
</div>
|
|
`;
|
|
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(
|
|
'<div><input type="checkbox"><input type="checkbox"><input type="checkbox"></div>'
|
|
);
|
|
|
|
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("in a t-foreach, part 2", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="state" t-as="thing" t-key="thing_index" >
|
|
<input t-model="state[thing_index]"/>
|
|
</t>
|
|
</div>
|
|
`;
|
|
state = useState(["zuko", "iroh"]);
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
expect(comp.state).toEqual(["zuko", "iroh"]);
|
|
|
|
const input = fixture.querySelectorAll("input")[1]!;
|
|
input.value = "uncle iroh";
|
|
input.dispatchEvent(new Event("input"));
|
|
expect(comp.state).toEqual(["zuko", "uncle iroh"]);
|
|
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`
|
|
<div t-key="'key'">
|
|
<input class="a" t-if="state.flag"/>
|
|
<input class="b" t-if="!state.flag"/>
|
|
</div>
|
|
`;
|
|
state = useState({ flag: true });
|
|
}
|
|
const comp = new SomeComponent();
|
|
await comp.mount(fixture);
|
|
expect(fixture.innerHTML).toBe('<div><input class="a"></div>');
|
|
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('<div><input class="b"></div>');
|
|
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`
|
|
<div>
|
|
<t t-if="env.someFlag">Red</t>
|
|
<t t-else="">Blue</t>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const app = new App();
|
|
await app.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div>Red</div>");
|
|
bus.trigger("some-event");
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>Blue</div>");
|
|
});
|
|
|
|
test("can define specific env for root components", async () => {
|
|
interface AppEnv extends Env {
|
|
test: number;
|
|
}
|
|
class App1 extends Component<{}, AppEnv> {
|
|
static template = xml`<span></span>`;
|
|
}
|
|
App1.env = { test: 1 };
|
|
class App2 extends Component<{}, AppEnv> {
|
|
static template = xml`<span></span>`;
|
|
}
|
|
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("top level sub widgets", () => {
|
|
test("basic use", async () => {
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<t t-name="Parent">
|
|
<Child p="1"/>
|
|
</t>
|
|
<span t-name="Child">child<t t-esc="props.p"/></span>
|
|
</templates>`);
|
|
class Child extends Component {}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<span>child1</span>");
|
|
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("sub widget is interactive", async () => {
|
|
env.qweb.addTemplates(`
|
|
<templates>
|
|
<t t-name="Parent">
|
|
<Child p="1"/>
|
|
</t>
|
|
<span t-name="Child"><button t-on-click="inc">click</button>child<t t-esc="state.val"/></span>
|
|
</templates>`);
|
|
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("<span><button>click</button>child1</span>");
|
|
const button = fixture.querySelector("button")!;
|
|
button.click();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<span><button>click</button>child2</span>");
|
|
});
|
|
|
|
test("can select a sub widget ", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span>CHILD 1</span>`;
|
|
}
|
|
class OtherChild extends Component {
|
|
static template = xml`<div>CHILD 2</div>`;
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t>
|
|
<t t-if="env.flag"><Child /></t>
|
|
<t t-if="!env.flag"><OtherChild /></t>
|
|
</t>
|
|
`;
|
|
static components = { Child, OtherChild };
|
|
}
|
|
(<any>env).flag = true;
|
|
let parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<span>CHILD 1</span>");
|
|
parent.destroy();
|
|
(<any>env).flag = false;
|
|
parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");
|
|
|
|
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`<span>CHILD 1</span>`;
|
|
}
|
|
class OtherChild extends Component {
|
|
static template = xml`<div>CHILD 2</div>`;
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t>
|
|
<t t-if="state.flag"><Child /></t>
|
|
<t t-if="!state.flag"><OtherChild /></t>
|
|
</t>
|
|
`;
|
|
state = useState({ flag: true });
|
|
static components = { Child, OtherChild };
|
|
}
|
|
let parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<span>CHILD 1</span>");
|
|
parent.state.flag = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");
|
|
});
|
|
|
|
test("top level sub widget with a parent", async () => {
|
|
class ComponentC extends Component {
|
|
static template = xml`<span>Hello</span>`;
|
|
}
|
|
class ComponentB extends Component {
|
|
static template = xml`<ComponentC />`;
|
|
static components = { ComponentC };
|
|
}
|
|
class ComponentA extends Component {
|
|
static template = xml`<div><ComponentB/></div>`;
|
|
static components = { ComponentB };
|
|
}
|
|
|
|
const component = new ComponentA();
|
|
await component.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>");
|
|
});
|
|
});
|
|
|
|
describe("dynamic root nodes", () => {
|
|
test("template with t-if, part 1", async () => {
|
|
class TestWidget extends Component {
|
|
static template = xml`
|
|
<t>
|
|
<t t-if="true"><span>hey</span></t>
|
|
<t t-if="false"><div>abc</div></t>
|
|
</t>
|
|
`;
|
|
}
|
|
|
|
const widget = new TestWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<span>hey</span>");
|
|
});
|
|
|
|
test("template with t-if, part 2", async () => {
|
|
class TestWidget extends Component {
|
|
static template = xml`
|
|
<t>
|
|
<t t-if="false"><span>hey</span></t>
|
|
<t t-if="true"><div>abc</div></t>
|
|
</t>
|
|
`;
|
|
}
|
|
|
|
const widget = new TestWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div>abc</div>");
|
|
});
|
|
|
|
test("switching between sub branches dynamically", async () => {
|
|
class TestWidget extends Component {
|
|
static template = xml`
|
|
<t>
|
|
<t t-if="state.flag"><span>hey</span></t>
|
|
<t t-if="!state.flag"><div>abc</div></t>
|
|
</t>
|
|
`;
|
|
state = useState({ flag: true });
|
|
}
|
|
|
|
const widget = new TestWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<span>hey</span>");
|
|
widget.state.flag = false;
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("<div>abc</div>");
|
|
});
|
|
|
|
test("switching between sub components dynamically", async () => {
|
|
class ChildA extends Component {
|
|
static template = xml`<span>hey</span>`;
|
|
}
|
|
class ChildB extends Component {
|
|
static template = xml`<div>abc</div>`;
|
|
}
|
|
class TestWidget extends Component {
|
|
static template = xml`
|
|
<t>
|
|
<t t-if="state.flag"><ChildA/></t>
|
|
<t t-if="!state.flag"><ChildB/></t>
|
|
</t>
|
|
`;
|
|
static components = { ChildA, ChildB };
|
|
state = useState({ flag: true });
|
|
}
|
|
|
|
const widget = new TestWidget();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<span>hey</span>");
|
|
widget.state.flag = false;
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("<div>abc</div>");
|
|
});
|
|
});
|
|
|
|
describe("dynamic t-props", () => {
|
|
test("basic use", async () => {
|
|
expect.assertions(4);
|
|
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<span>
|
|
<t t-esc="props.a + props.b"/>
|
|
</span>
|
|
`;
|
|
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`
|
|
<div>
|
|
<Child t-props="some.obj"/>
|
|
</div>
|
|
`;
|
|
static components = { Child };
|
|
|
|
some = { obj: { a: 1, b: 2 } };
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
|
|
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-props with props", async () => {
|
|
expect.assertions(1);
|
|
|
|
class Child extends Component {
|
|
static template = xml`<div />`;
|
|
setup() {
|
|
expect(this.props).toEqual({ a: 1, b: 2, c: "c" });
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<Child t-props="props" a="1" b="2" />
|
|
</div>
|
|
`;
|
|
static components = { Child };
|
|
|
|
props = { a: "a", c: "c" };
|
|
}
|
|
|
|
const widget = new Parent();
|
|
await widget.mount(fixture);
|
|
});
|
|
});
|
|
|
|
describe("support svg components", () => {
|
|
test("add proper namespace to svg", async () => {
|
|
class GComp extends Component {
|
|
static template = xml`
|
|
<g>
|
|
<circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/>
|
|
</g>`;
|
|
}
|
|
|
|
class Svg extends Component {
|
|
static template = xml`
|
|
<svg>
|
|
<GComp/>
|
|
</svg>`;
|
|
static components = { GComp };
|
|
}
|
|
const widget = new Svg();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
'<svg><g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"></circle></g></svg>'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("t-raw in components", () => {
|
|
test("update properly on state changes", async () => {
|
|
class TestW extends Component {
|
|
static template = xml`<div><t t-raw="state.value"/></div>`;
|
|
state = useState({ value: "<b>content</b>" });
|
|
}
|
|
const widget = new TestW();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><b>content</b></div>");
|
|
|
|
widget.state.value = "<span>other content</span>";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>other content</span></div>");
|
|
});
|
|
|
|
test("can render list of t-raw ", async () => {
|
|
class TestW extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="state.items" t-as="item">
|
|
<t t-esc="item"/>
|
|
<t t-raw="item"/>
|
|
</t>
|
|
</div>`;
|
|
state = useState({ items: ["<b>one</b>", "<b>two</b>", "<b>tree</b>"] });
|
|
}
|
|
const widget = new TestW();
|
|
await widget.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><b>one</b><b>one</b><b>two</b><b>two</b><b>tree</b><b>tree</b></div>"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("t-call", () => {
|
|
test("handlers are properly bound through a t-call", async () => {
|
|
expect.assertions(3);
|
|
env.qweb.addTemplate("sub", `<p t-on-click="update">lucas</p>`);
|
|
class Parent extends Component {
|
|
static template = xml`<div><t t-call="sub"/></div>`;
|
|
update() {
|
|
expect(this).toBe(parent);
|
|
}
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>lucas</p></div>");
|
|
fixture.querySelector("p")!.click();
|
|
const subId = env.qweb.subTemplates["sub"];
|
|
const subFn = QWeb.subTemplates[subId] as any;
|
|
expect(subFn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("parent is set within t-call", async () => {
|
|
env.qweb.addTemplate("sub", `<Child/>`);
|
|
let child;
|
|
class Child extends Component {
|
|
static template = xml`<span>lucas</span>`;
|
|
constructor() {
|
|
super(...arguments);
|
|
child = this;
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`<div><t t-call="sub"/></div>`;
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>lucas</span></div>");
|
|
expect(child.__owl__.parent).toBe(parent);
|
|
const subId = env.qweb.subTemplates["sub"];
|
|
const subFn = QWeb.subTemplates[subId] as any;
|
|
|
|
expect(subFn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("t-call in t-foreach and children component", async () => {
|
|
env.qweb.addTemplate("sub", `<Child val="val"/>`);
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="['a', 'b', 'c']" t-as="val" t-key="val">
|
|
<t t-call="sub"/>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>a</span><span>b</span><span>c</span></div>");
|
|
});
|
|
|
|
test("parent is set within t-call with no parentNode", async () => {
|
|
env.qweb.addTemplate("sub", `<Child/>`);
|
|
let child;
|
|
class Child extends Component {
|
|
constructor() {
|
|
super(...arguments);
|
|
child = this;
|
|
}
|
|
static template = xml`<span>lucas</span>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`<t t-call="sub"/>`;
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<span>lucas</span>");
|
|
expect(child.__owl__.parent).toBe(parent);
|
|
const subId = env.qweb.subTemplates["sub"];
|
|
const subFn = QWeb.subTemplates[subId] as any;
|
|
expect(subFn.toString()).toMatchSnapshot();
|
|
});
|
|
|
|
test("handlers with arguments are properly bound through a t-call", async () => {
|
|
expect.assertions(3);
|
|
env.qweb.addTemplate("sub", `<p t-on-click="update(a)">lucas</p>`);
|
|
class Parent extends Component {
|
|
static template = xml`<div><t t-call="sub"/></div>`;
|
|
update(a) {
|
|
expect(this).toBe(parent);
|
|
expect(a).toBe(3);
|
|
}
|
|
a = 3;
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
const subId = env.qweb.subTemplates["sub"];
|
|
const subFn = QWeb.subTemplates[subId] as any;
|
|
expect(subFn.toString()).toMatchSnapshot();
|
|
|
|
fixture.querySelector("p")!.click();
|
|
});
|
|
|
|
test("sub components in two t-calls", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
|
}
|
|
|
|
env.qweb.addTemplate("sub", `<Child val="state.val"/>`);
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-if="state.val===1">
|
|
<t t-call="sub"/>
|
|
</t>
|
|
<div t-else=""><t t-call="sub"/></div>
|
|
</div>`;
|
|
static components = { Child };
|
|
state = useState({ val: 1 });
|
|
}
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
parent.state.val = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><div><span>2</span></div></div>");
|
|
});
|
|
});
|