mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
1ae9d514b9
With this commit, component only render child components if they have different props (shallow equality). Otherwise, we trust the reactivity system to make sure that all impacted components are updated
443 lines
10 KiB
TypeScript
443 lines
10 KiB
TypeScript
import { Component, mount, onRendered, onWillUpdateProps, useState, xml } from "../../src";
|
|
import {
|
|
makeTestFixture,
|
|
snapshotEverything,
|
|
nextTick,
|
|
useLogLifecycle,
|
|
makeDeferred,
|
|
nextMicroTick,
|
|
} from "../helpers";
|
|
|
|
let fixture: HTMLElement;
|
|
|
|
snapshotEverything();
|
|
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
});
|
|
|
|
describe("rendering semantics", () => {
|
|
test("can render a parent without rendering child", async () => {
|
|
class Child extends Component {
|
|
static template = xml`child`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-esc="state.value"/>
|
|
<Child/>
|
|
`;
|
|
static components = { Child };
|
|
|
|
state = useState({ value: "A" });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("Achild");
|
|
expect([
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]).toBeLogged();
|
|
|
|
parent.state.value = "B";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("Bchild");
|
|
expect([
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Parent:willPatch",
|
|
"Parent:patched",
|
|
]).toBeLogged();
|
|
});
|
|
|
|
test("can force a render to update sub tree", async () => {
|
|
let childN = 0;
|
|
let parentN = 0;
|
|
class Child extends Component {
|
|
static template = xml`child`;
|
|
setup() {
|
|
onRendered(() => childN++);
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-esc="state.value"/>
|
|
<Child/>
|
|
`;
|
|
static components = { Child };
|
|
|
|
state = { value: "A" };
|
|
setup() {
|
|
onRendered(() => parentN++);
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("Achild");
|
|
expect(parentN).toBe(1);
|
|
expect(childN).toBe(1);
|
|
|
|
parent.state.value = "B";
|
|
parent.render(true);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("Bchild");
|
|
expect(parentN).toBe(2);
|
|
expect(childN).toBe(2);
|
|
});
|
|
|
|
test("render with deep=true followed by render with deep=false work as expected", async () => {
|
|
class Child extends Component {
|
|
static template = xml`child<t t-esc="env.getValue()"/>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`parent<t t-esc="state.value"/><Child/>`;
|
|
static components = { Child };
|
|
|
|
state = useState({ value: "A" });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
let value = 3;
|
|
const env = {
|
|
getValue() {
|
|
return value;
|
|
},
|
|
};
|
|
|
|
const parent = await mount(Parent, fixture, { env });
|
|
|
|
expect(fixture.innerHTML).toBe("parentAchild3");
|
|
expect([
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]).toBeLogged();
|
|
|
|
value = 4;
|
|
parent.render(true);
|
|
|
|
// wait for child to be rendered, but dom not yet patched
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect([
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
]).toBeLogged();
|
|
|
|
parent.state.value = "B";
|
|
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("parentBchild4");
|
|
expect([
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Parent:willPatch",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
"Parent:patched",
|
|
]).toBeLogged();
|
|
});
|
|
|
|
test("props are reactive", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.a.b"/>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<Child a="state"/>
|
|
`;
|
|
static components = { Child };
|
|
|
|
state = useState({ b: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("1");
|
|
expect([
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]).toBeLogged();
|
|
|
|
parent.state.b = 3;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("3");
|
|
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
|
|
});
|
|
|
|
test("props are reactive (nested prop)", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.a.b.c"/>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<Child a="state"/>
|
|
`;
|
|
static components = { Child };
|
|
|
|
state = useState({ b: { c: 1 } });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("1");
|
|
expect([
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]).toBeLogged();
|
|
|
|
parent.state.b.c = 3; // parent is now subscribed to 'b' key
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("3");
|
|
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
|
|
|
|
parent.state.b = { c: 444 }; // triggers a parent and a child render
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("444");
|
|
expect([
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Parent:willPatch",
|
|
"Parent:patched",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
]).toBeLogged();
|
|
});
|
|
|
|
test("works as expected for dynamic number of props", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="Object.keys(props).length"/>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<Child t-props="state"/>
|
|
`;
|
|
static components = { Child };
|
|
|
|
state: any = useState({ b: 1 });
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("1");
|
|
parent.state.newkey = 3;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("2");
|
|
});
|
|
|
|
test("rendering is atomic (for one subtree)", async () => {
|
|
const def = makeDeferred();
|
|
|
|
class C extends Component {
|
|
static template = xml`<t t-esc="props.obj.val"/>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class B extends Component {
|
|
static template = xml`<C obj="props.obj"/>`;
|
|
static components = { C };
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => def);
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`<t t-esc="state.obj.val"/><B obj="state.obj"/>`;
|
|
static components = { B };
|
|
|
|
state = useState({ obj: { val: 1 } });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("11");
|
|
expect([
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]).toBeLogged();
|
|
|
|
parent.state.obj.val = 3;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("33");
|
|
expect([
|
|
"A:willRender",
|
|
"A:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"A:willPatch",
|
|
"A:patched",
|
|
"C:willPatch",
|
|
"C:patched",
|
|
]).toBeLogged();
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
expect([]).toBeLogged();
|
|
});
|
|
});
|
|
|
|
test("force render in case of existing render", async () => {
|
|
const def = makeDeferred();
|
|
|
|
class C extends Component {
|
|
static template = xml`C`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
class B extends Component {
|
|
static template = xml`<C/><t t-esc="props.val"/>`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => def);
|
|
}
|
|
}
|
|
class A extends Component {
|
|
static template = xml`<B val="state.val"/>`;
|
|
static components = { B };
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("C1");
|
|
expect([
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]).toBeLogged();
|
|
|
|
// trigger a new rendering, blocked in B
|
|
parent.state.val = 2;
|
|
await nextTick();
|
|
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
|
|
|
|
// initiate a new render with deep=true. it should cancel the current render
|
|
// and also be blocked in B
|
|
parent.render(true);
|
|
await nextTick();
|
|
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
// we check here that the render reaches C (so, that it was properly forced)
|
|
expect([
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"C:willPatch",
|
|
"C:patched",
|
|
"B:patched",
|
|
"A:patched",
|
|
]).toBeLogged();
|
|
});
|