Files
owl/tests/components/props.test.ts
T
Géry Debongnie a51b286671 [IMP] implement .alike suffix on props
It is common in Owl components to have anonymous function as props.
However, since each rendering create a different (but equivalent)
closure, Owl will consider the props different, so will update the child
component, but this is (often) not necessary.

This commit will help reduce the problem by introducing a new `.alike`
prop suffix, that will let Owl know that each version of that specific
prop should be considered the same, so, will be ignored by the props
comparison code.

closes #1360
2023-03-09 11:14:00 +01:00

399 lines
10 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>&lt;p&gt;43&lt;/p&gt;<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("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();
});