mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
0024f33fa1
It is not really useful, but it is possible to bind an anonymous function prop. However, with the recent change on how the bind feature works, it now crashes. This commit makes sure that we properly apply the `bind` operation to the function, and not to the last term of the function.
424 lines
11 KiB
TypeScript
424 lines
11 KiB
TypeScript
import { Component, mount, onWillUpdateProps, useState, xml } from "../../src";
|
|
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
|
|
|
|
let fixture: HTMLElement;
|
|
|
|
snapshotEverything();
|
|
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
});
|
|
|
|
describe("basics", () => {
|
|
test("explicit object prop", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="state.someval"/></span>`;
|
|
state: any;
|
|
setup() {
|
|
this.state = useState({ someval: this.props.value });
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child value="state.val"/></div>`;
|
|
static components = { Child };
|
|
state = useState({ val: 42 });
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
|
});
|
|
|
|
test("prop names can contain -", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<div><t t-esc="props['prop-name']"/></div>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child prop-name="7"/>`;
|
|
static components = { Child };
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>7</div>");
|
|
});
|
|
|
|
test("accept ES6-like syntax for props (with getters)", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.greetings"/></span>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child greetings="greetings"/></div>`;
|
|
static components = { Child };
|
|
get greetings() {
|
|
const name = "aaron";
|
|
return `hello ${name}`;
|
|
}
|
|
}
|
|
await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>hello aaron</span></div>");
|
|
});
|
|
|
|
test("t-set works ", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-set="val" t-value="42"/>
|
|
<Child val="val"/>
|
|
</div>`;
|
|
static components = { Child };
|
|
}
|
|
await mount(Parent, fixture);
|
|
expect(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>`;
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
|
});
|
|
|
|
test("t-set with a body expression can be passed in props, and then t-out", async () => {
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<span>
|
|
<t t-esc="props.val"/>
|
|
<t t-out="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>`;
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span><p>43</p><p>43</p></span></div>");
|
|
});
|
|
|
|
test("arrow functions as prop correctly capture their scope", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<button t-on-click="props.onClick"/>`;
|
|
}
|
|
|
|
let onClickArgs: [number, MouseEvent] | null = null;
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-foreach="items" t-as="item" t-key="item.val">
|
|
<Child onClick="ev => onClick(item.val, ev)"/>
|
|
</t>
|
|
`;
|
|
static components = { Child };
|
|
items = [{ val: 1 }, { val: 2 }, { val: 3 }, { val: 4 }];
|
|
onClick(n: number, ev: MouseEvent) {
|
|
onClickArgs = [n, ev];
|
|
}
|
|
}
|
|
await mount(Parent, fixture);
|
|
expect(onClickArgs).toBeNull();
|
|
(<HTMLElement>fixture.querySelector("button")).click();
|
|
expect(onClickArgs![0]).toBe(1);
|
|
expect(onClickArgs![1]).toBeInstanceOf(MouseEvent);
|
|
});
|
|
|
|
test("support prop names that aren't valid bare object property names", async () => {
|
|
expect.assertions(3);
|
|
class Child extends Component {
|
|
static template = xml`<button t-on-click="props.onClick"/>`;
|
|
setup() {
|
|
expect(this.props["some-dashed-prop"]).toBe(5);
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child some-dashed-prop="5"/>`;
|
|
static components = { Child };
|
|
}
|
|
await mount(Parent, fixture);
|
|
});
|
|
|
|
test("template string in prop", async () => {
|
|
expect.assertions(3);
|
|
class Child extends Component {
|
|
static template = xml``;
|
|
setup() {
|
|
expect(this.props.propName).toBe("123");
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml({ raw: ['<Child propName="`1${someVal}3`"/>'] });
|
|
static components = { Child };
|
|
someVal = 2;
|
|
}
|
|
await mount(Parent, fixture);
|
|
});
|
|
});
|
|
|
|
test("can bind function prop with bind suffix", async () => {
|
|
class Child extends Component {
|
|
static template = xml`child`;
|
|
setup() {
|
|
this.props.doSomething(123);
|
|
}
|
|
}
|
|
|
|
let boundedThing: any = null;
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child doSomething.bind="doSomething"/>`;
|
|
static components = { Child };
|
|
|
|
doSomething(val: number) {
|
|
expect(val).toBe(123);
|
|
boundedThing = this;
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(boundedThing).toBe(parent);
|
|
expect(fixture.innerHTML).toBe("child");
|
|
});
|
|
|
|
test("do not crash when binding anonymous function prop with bind suffix", async () => {
|
|
class Child extends Component {
|
|
static template = xml`child`;
|
|
setup() {
|
|
this.props.doSomething(123);
|
|
}
|
|
}
|
|
|
|
let boundedThing: any = null;
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child doSomething.bind="(val) => this.doSomething(val)"/>`;
|
|
static components = { Child };
|
|
|
|
doSomething(val: number) {
|
|
expect(val).toBe(123);
|
|
boundedThing = this;
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(boundedThing).toBe(parent);
|
|
expect(fixture.innerHTML).toBe("child");
|
|
});
|
|
|
|
test("bound functions is not referentially equal after update", async () => {
|
|
let isEqual = false;
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.val"/>`;
|
|
setup() {
|
|
onWillUpdateProps((nextProps: any) => {
|
|
isEqual = nextProps.fn === this.props.fn;
|
|
});
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child val="state.val" fn.bind="someFunction"/>`;
|
|
static components = { Child };
|
|
state = useState({ val: 1 });
|
|
someFunction() {}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
parent.state.val = 3;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("3");
|
|
expect(isEqual).toBe(false);
|
|
});
|
|
|
|
test("bound functions are considered 'alike'", async () => {
|
|
class Child extends Component {
|
|
static template = xml`child`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-esc="state.val"/>
|
|
<Child fn.bind="someFunction"/>`;
|
|
static components = { Child };
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
someFunction() {}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("1child");
|
|
expect([
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]).toBeLogged();
|
|
parent.state.val = 3;
|
|
await nextTick();
|
|
expect([
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Parent:willPatch",
|
|
"Parent:patched",
|
|
]).toBeLogged();
|
|
expect(fixture.innerHTML).toBe("3child");
|
|
});
|
|
|
|
test("throw if prop uses an unknown suffix", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.val"/>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child val.somesuffix="state.val"/>`;
|
|
static components = { Child };
|
|
}
|
|
|
|
await expect(async () => {
|
|
await mount(Parent, fixture);
|
|
}).rejects.toThrowError("Invalid prop suffix");
|
|
});
|
|
|
|
test(".alike suffix in a simple case", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.fn()"/>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-esc="state.counter"/>
|
|
<Child fn.alike="() => 1"/>`;
|
|
static components = { Child };
|
|
state = useState({ counter: 0 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect([
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]).toBeLogged();
|
|
|
|
expect(fixture.innerHTML).toBe("01");
|
|
parent.state.counter++;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("11");
|
|
expect([
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Parent:willPatch",
|
|
"Parent:patched",
|
|
]).toBeLogged();
|
|
});
|
|
|
|
test(".alike suffix in a list", async () => {
|
|
class Todo extends Component {
|
|
static template = xml`
|
|
<button t-on-click="props.toggle">
|
|
<t t-esc="props.todo.id"/><t t-if="props.todo.isChecked">V</t>
|
|
</button>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-foreach="state.elems" t-as="elem" t-key="elem.id">
|
|
<Todo todo="elem" toggle.alike="() => this.toggle(elem.id)"/>
|
|
</t>`;
|
|
static components = { Todo };
|
|
state = useState({
|
|
elems: [
|
|
{ id: 1, isChecked: false },
|
|
{ id: 2, isChecked: true },
|
|
],
|
|
});
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
toggle(id: number) {
|
|
const todo = this.state.elems.find((el) => el.id === id)!;
|
|
todo.isChecked = !todo.isChecked;
|
|
}
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect([
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Todo:setup",
|
|
"Todo:willStart",
|
|
"Todo:setup",
|
|
"Todo:willStart",
|
|
"Parent:rendered",
|
|
"Todo:willRender",
|
|
"Todo:rendered",
|
|
"Todo:willRender",
|
|
"Todo:rendered",
|
|
"Todo:mounted",
|
|
"Todo:mounted",
|
|
"Parent:mounted",
|
|
]).toBeLogged();
|
|
|
|
expect(fixture.innerHTML).toBe("<button>1</button><button>2V</button>");
|
|
fixture.querySelector("button")?.click();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<button>1V</button><button>2V</button>");
|
|
expect([
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Todo:willRender",
|
|
"Todo:rendered",
|
|
"Todo:willPatch",
|
|
"Todo:patched",
|
|
"Parent:willPatch",
|
|
"Parent:patched",
|
|
]).toBeLogged();
|
|
});
|