Files
owl/tests/component/async.test.ts
T
2020-04-21 16:23:31 +02:00

1409 lines
44 KiB
TypeScript

import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextMicroTick, nextTick } 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();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
function children(w: Component): Component[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map((id) => childrenMap[id]);
}
describe("async rendering", () => {
test("destroying a widget before start is over", async () => {
let def = makeDeferred();
class W extends Component {
static template = xml`<div/>`;
willStart(): Promise<void> {
return def;
}
}
const w = new W();
w.mount(fixture);
expect(w.__owl__.isDestroyed).toBe(false);
expect(w.__owl__.isMounted).toBe(false);
w.destroy();
def.resolve();
await nextTick();
expect(w.__owl__.isDestroyed).toBe(true);
expect(w.__owl__.isMounted).toBe(false);
});
test("destroying/recreating a subwidget with different props (if start is not over)", async () => {
let def = makeDeferred();
let n = 0;
class Child extends Component {
static template = xml`<span>child:<t t-esc="props.val"/></span>`;
constructor(parent, props) {
super(parent, props);
n++;
}
willStart(): Promise<void> {
return def;
}
}
class W extends Component {
static template = xml`<div><t t-if="state.val > 1"><Child val="state.val"/></t></div>`;
static components = { Child };
state = useState({ val: 1 });
}
const w = new W();
await w.mount(fixture);
expect(n).toBe(0);
w.state.val = 2;
await nextMicroTick();
expect(n).toBe(1);
w.state.val = 3;
await nextMicroTick();
expect(n).toBe(2);
def.resolve();
await nextTick();
expect(children(w).length).toBe(1);
expect(fixture.innerHTML).toBe("<div><span>child:3</span></div>");
});
test("creating two async components, scenario 1", async () => {
let defA = makeDeferred();
let defB = makeDeferred();
let nbRenderings: number = 0;
class ChildA extends Component {
static template = xml`<span><t t-esc="getValue()"/></span>`;
willStart(): Promise<void> {
return defA;
}
getValue() {
nbRenderings++;
return "a";
}
}
class ChildB extends Component {
static template = xml`<span>b</span>`;
willStart(): Promise<void> {
return defB;
}
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="state.flagA"><ChildA /></t>
<t t-if="state.flagB"><ChildB /></t>
</div>`;
static components = { ChildA, ChildB };
state = useState({ flagA: false, flagB: false });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
parent.state.flagA = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
parent.state.flagB = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
defB.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(nbRenderings).toBe(0);
defA.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>a</span><span>b</span></div>");
expect(nbRenderings).toBe(1);
});
test("creating two async components, scenario 2", async () => {
let defA = makeDeferred();
let defB = makeDeferred();
class ChildA extends Component {
static template = xml`<span>a<t t-esc="props.val"/></span>`;
willUpdateProps(): Promise<void> {
return defA;
}
}
class ChildB extends Component {
static template = xml`<span>b<t t-esc="props.val"/></span>`;
willStart(): Promise<void> {
return defB;
}
}
class Parent extends Component {
static template = xml`
<div>
<ChildA val="state.valA"/>
<t t-if="state.flagB"><ChildB val="state.valB"/></t>
</div>`;
static components = { ChildA, ChildB };
state = useState({ valA: 1, valB: 2, flagB: false });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
parent.state.valA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
parent.state.flagB = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
defB.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
defA.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>a2</span><span>b2</span></div>");
});
test("creating two async components, scenario 3 (patching in the same frame)", async () => {
let defA = makeDeferred();
let defB = makeDeferred();
class ChildA extends Component {
static template = xml`<span>a<t t-esc="props.val"/></span>`;
willUpdateProps(): Promise<void> {
return defA;
}
}
class ChildB extends Component {
static template = xml`<span>b<t t-esc="props.val"/></span>`;
willStart(): Promise<void> {
return defB;
}
}
class Parent extends Component {
static template = xml`
<div>
<ChildA val="state.valA"/>
<t t-if="state.flagB"><ChildB val="state.valB"/></t>
</div>`;
static components = { ChildA, ChildB };
state = useState({ valA: 1, valB: 2, flagB: false });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
parent.state.valA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
parent.state.flagB = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
defB.resolve();
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
defA.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>a2</span><span>b2</span></div>");
});
test("update a sub-component twice in the same frame", async () => {
const steps: string[] = [];
const defs = [makeDeferred(), makeDeferred()];
let index = 0;
class ChildA extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
willUpdateProps(): Promise<void> {
return defs[index++];
}
patched() {
steps.push("patched");
}
}
class Parent extends Component {
static template = xml`<div><ChildA val="state.valA"/></div>`;
static components = { ChildA };
state = useState({ valA: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.state.valA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.state.valA = 3;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
defs[0].resolve();
await Promise.resolve();
defs[1].resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(steps).toEqual(["patched"]);
});
test("update a sub-component twice in the same frame, 2", async () => {
const steps: string[] = [];
class ChildA extends Component {
static template = xml`<span><t t-esc="val()"/></span>`;
patched() {
steps.push("patched");
}
val() {
steps.push("render");
return this.props.val;
}
}
class Parent extends Component {
static template = xml`<div><ChildA val="state.valA"/></div>`;
static components = { ChildA };
state = useState({ valA: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.state.valA = 2;
await nextMicroTick();
expect(steps).toEqual(["render"]);
await nextMicroTick();
// For an unknown reason, this test fails on windows without the next microtick. It works
// in linux and osx, but fails on at least this machine.
// I do not see anything harmful in waiting an extra tick. But it is annoying to not
// know what is different.
await nextMicroTick();
expect(steps).toEqual(["render", "render"]);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.state.valA = 3;
await nextMicroTick();
expect(steps).toEqual(["render", "render"]);
await nextMicroTick();
// same as above
await nextMicroTick();
expect(steps).toEqual(["render", "render", "render"]);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(steps).toEqual(["render", "render", "render", "patched"]);
});
test("components in a node in a t-foreach ", async () => {
class Child extends Component {}
class App extends Component {
static components = { Child };
get items() {
return [1, 2];
}
}
env.qweb.addTemplates(`
<templates>
<div t-name="Child"><t t-esc="props.item"/></div>
<div t-name="App">
<ul>
<t t-foreach="items" t-as="item">
<li t-key="'li_'+item">
<Child item="item"/>
</li>
</t>
</ul>
</div>
</templates>`);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><ul><li><div>1</div></li><li><div>2</div></li></ul></div>"
);
});
test("properly behave when destroyed/unmounted while rendering ", async () => {
const def = makeDeferred();
class SubChild extends Component {
static template = xml`<div/>`;
willPatch() {
throw new Error("Should not happen!");
}
patched() {
throw new Error("Should not happen!");
}
willUpdateProps() {
return def;
}
}
class Child extends Component {
static template = xml`<div><SubChild /></div>`;
static components = { SubChild };
}
class Parent extends Component {
static template = xml`<div><t t-if="state.flag"><Child val="state.val"/></t></div>`;
static components = { Child };
state = useState({ flag: true, val: "Framboise Lindemans" });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div></div></div></div>");
// this change triggers a rendering of the parent. This rendering is delayed,
// because child is now waiting for def to be resolved
parent.state.val = "Framboise Girardin";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div><div></div></div></div>");
// with this, we remove child, and subchild, even though it is not finished
// rendering from previous changes
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
// we now resolve def, so the child rendering is now complete.
(<any>def).resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
});
test.skip("reuse widget if possible, in some async situation", async () => {
// this optimization has been temporarily deactivated
env.qweb.addTemplates(`
<templates>
<span t-name="ChildA">a<t t-esc="props.val"/></span>
<span t-name="ChildB">b<t t-esc="props.val"/></span>
<span t-name="Parent">
<t t-if="state.flag">
<ChildA val="state.valA"/>
<ChildB val="state.valB"/>
</t>
</span>
</templates>
`);
let destroyCount = 0;
class ChildA extends Component {
destroy() {
destroyCount++;
super.destroy();
}
}
class ChildB extends Component {
willStart(): any {
return new Promise(function () {});
}
}
class Parent extends Component {
static components = { ChildA, ChildB };
state = useState({ valA: 1, valB: 2, flag: false });
}
const parent = new Parent();
await parent.mount(fixture);
expect(destroyCount).toBe(0);
parent.state.flag = true;
await nextTick();
expect(destroyCount).toBe(0);
parent.state.valB = 3;
await nextTick();
expect(destroyCount).toBe(0);
});
test("rendering component again in next microtick", async () => {
class Child extends Component {
static template = xml`<div t-name="Child">Child</div>`;
}
class App extends Component {
static template = xml`
<div>
<button t-on-click="onClick">Click</button>
<t t-if="env.flag"><Child/></t>
</div>`;
static components = { Child };
async onClick() {
(env as any).flag = true;
this.render();
await Promise.resolve();
this.render();
}
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Click</button></div>");
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>Click</button><div>Child</div></div>");
});
test("concurrent renderings scenario 1", async () => {
const def = makeDeferred();
let stateB;
class ComponentC extends Component {
static template = xml`<span><t t-esc="props.fromA"/><t t-esc="someValue()"/></span>`;
someValue() {
return this.props.fromB;
}
willUpdateProps() {
return def;
}
}
ComponentC.prototype.someValue = jest.fn(ComponentC.prototype.someValue);
class ComponentB extends Component {
static components = { ComponentC };
static template = xml`<p><ComponentC fromA="props.fromA" fromB="state.fromB" /></p>`;
state = useState({ fromB: "b" });
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
state = useState({ fromA: 1 });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
expect(ComponentC.prototype.someValue).toBeCalledTimes(1);
def.resolve();
await nextTick();
expect(ComponentC.prototype.someValue).toBeCalledTimes(2);
expect(fixture.innerHTML).toBe("<div><p><span>2c</span></p></div>");
});
test("concurrent renderings scenario 2", async () => {
// this test asserts that a rendering initiated before another one, and that
// ends after it, is re-mapped to that second rendering
const defs = [makeDeferred(), makeDeferred()];
let index = 0;
let stateB;
class ComponentC extends Component {
static template = xml`<span><t t-esc="props.fromA"/><t t-esc="props.fromB"/></span>`;
willUpdateProps() {
return defs[index++];
}
}
class ComponentB extends Component {
static template = xml`<p><ComponentC fromA="props.fromA" fromB="state.fromB" /></p>`;
static components = { ComponentC };
state = useState({ fromB: "b" });
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
}
class ComponentA extends Component {
static template = xml`<div><t t-esc="state.fromA"/><ComponentB fromA="state.fromA"/></div>`;
static components = { ComponentB };
state = useState({ fromA: 1 });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1<p><span>1b</span></p></div>");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div>1<p><span>1b</span></p></div>");
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("<div>1<p><span>1b</span></p></div>");
defs[1].resolve(); // resolve rendering initiated in B
await nextTick();
expect(fixture.innerHTML).toBe("<div>2<p><span>2c</span></p></div>");
defs[0].resolve(); // resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("<div>2<p><span>2c</span></p></div>");
});
test("concurrent renderings scenario 2bis", async () => {
const defs = [makeDeferred(), makeDeferred()];
let index = 0;
let stateB;
class ComponentC extends Component {
static template = xml`<span><t t-esc="props.fromA"/><t t-esc="props.fromB"/></span>`;
willUpdateProps() {
return defs[index++];
}
}
class ComponentB extends Component {
static template = xml`<p><ComponentC fromA="props.fromA" fromB="state.fromB" /></p>`;
static components = { ComponentC };
state = useState({ fromB: "b" });
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
}
class ComponentA extends Component {
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
static components = { ComponentB };
state = useState({ fromA: 1 });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
defs[0].resolve(); // resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>"); // TODO: is this what we want?? 2b could be ok too
defs[1].resolve(); // resolve rendering initiated in B
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>2c</span></p></div>");
});
test("concurrent renderings scenario 3", async () => {
const defB = makeDeferred();
const defsD = [makeDeferred(), makeDeferred()];
let index = 0;
let stateC;
class ComponentD extends Component {
static template = xml`<i><t t-esc="props.fromA"/><t t-esc="someValue()"/></i>`;
someValue() {
return this.props.fromC;
}
willUpdateProps() {
return defsD[index++];
}
}
ComponentD.prototype.someValue = jest.fn(ComponentD.prototype.someValue);
class ComponentC extends Component {
static template = xml`<span><ComponentD fromA="props.fromA" fromC="state.fromC" /></span>`;
static components = { ComponentD };
state = useState({ fromC: "c" });
constructor(parent, props) {
super(parent, props);
stateC = this.state;
}
}
class ComponentB extends Component {
static template = xml`<p><ComponentC fromA="props.fromA" /></p>`;
static components = { ComponentC };
willUpdateProps() {
return defB;
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
state = useState({ fromA: 1 });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
stateC.fromC = "d";
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(ComponentD.prototype.someValue).toBeCalledTimes(1);
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
defsD[1].resolve(); // completely resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>2d</i></span></p></div>");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 4", async () => {
const defB = makeDeferred();
const defsD = [makeDeferred(), makeDeferred()];
let index = 0;
let stateC;
class ComponentD extends Component {
static template = xml`<i><t t-esc="props.fromA"/><t t-esc="someValue()"/></i>`;
someValue() {
return this.props.fromC;
}
willUpdateProps() {
return defsD[index++];
}
}
ComponentD.prototype.someValue = jest.fn(ComponentD.prototype.someValue);
class ComponentC extends Component {
static template = xml`<span><ComponentD fromA="props.fromA" fromC="state.fromC" /></span>`;
static components = { ComponentD };
state = useState({ fromC: "c" });
constructor(parent, props) {
super(parent, props);
stateC = this.state;
}
}
class ComponentB extends Component {
static template = xml`<p><ComponentC fromA="props.fromA" /></p>`;
static components = { ComponentC };
willUpdateProps() {
return defB;
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
state = useState({ fromA: 1 });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
stateC.fromC = "d";
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
defsD[1].resolve(); // completely resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>2d</i></span></p></div>");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>2d</i></span></p></div>");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 5", async () => {
const defsB = [makeDeferred(), makeDeferred()];
let index = 0;
class ComponentB extends Component {
static template = xml`<p><t t-esc="someValue()" /></p>`;
someValue() {
return this.props.fromA;
}
willUpdateProps() {
return defsB[index++];
}
}
ComponentB.prototype.someValue = jest.fn(ComponentB.prototype.someValue);
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
state = useState({ fromA: 1 });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
component.state.fromA = 3;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
defsB[0].resolve(); // resolve first re-rendering (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
expect(ComponentB.prototype.someValue).toBeCalledTimes(1);
defsB[1].resolve(); // resolve second re-rendering
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 6", async () => {
const defsB = [makeDeferred(), makeDeferred()];
let index = 0;
class ComponentB extends Component {
static template = xml`<p><t t-esc="someValue()" /></p>`;
someValue() {
return this.props.fromA;
}
willUpdateProps() {
return defsB[index++];
}
}
ComponentB.prototype.someValue = jest.fn(ComponentB.prototype.someValue);
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
state = useState({ fromA: 1 });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
component.state.fromA = 3;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
defsB[1].resolve(); // resolve second re-rendering
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
defsB[0].resolve(); // resolve first re-rendering (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 7", async () => {
class ComponentB extends Component {
static template = xml`<p><t t-esc="props.fromA" /><t t-esc="someValue()" /></p>`;
state = useState({ fromB: "b" });
someValue() {
return this.state.fromB;
}
async willUpdateProps() {
this.state.fromB = "c";
}
}
ComponentB.prototype.someValue = jest.fn(ComponentB.prototype.someValue);
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
state = useState({ fromA: 1 });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>1b</p></div>");
expect(ComponentB.prototype.someValue).toBeCalledTimes(1);
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>2c</p></div>");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 8", async () => {
const def = makeDeferred();
let stateB;
class ComponentB extends Component {
static template = xml`<p><t t-esc="props.fromA" /><t t-esc="state.fromB" /></p>`;
state = useState({ fromB: "b" });
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
async willUpdateProps(nextProps) {
return def;
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
state = useState({ fromA: 1 });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>1b</p></div>");
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>1b</p></div>");
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>1b</p></div>");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>2c</p></div>");
});
test("concurrent renderings scenario 9", async () => {
// Here is the global idea of this scenario:
// A
// / \
// B C
// |
// D
// A state is updated, triggering a whole re-rendering
// B is async, and blocked
// C (and D) are rendered
// C state is updated, producing a re-rendering of C and D
// this last re-rendering of C should be correctly re-mapped to the whole
// re-rendering
const def = makeDeferred();
let stateC;
class ComponentD extends Component {
static template = xml`<span><t t-esc="props.fromA"/><t t-esc="props.fromC"/></span>`;
}
class ComponentC extends Component {
static template = xml`<p><ComponentD fromA="props.fromA" fromC="state.fromC" /></p>`;
static components = { ComponentD };
state = useState({ fromC: "b1" });
constructor(parent, props) {
super(parent, props);
stateC = this.state;
}
}
class ComponentB extends Component {
static template = xml`<b><t t-esc="props.fromA"/></b>`;
willUpdateProps() {
return def;
}
}
class ComponentA extends Component {
static template = xml`
<div>
<t t-esc="state.fromA"/>
<ComponentB fromA="state.fromA"/>
<ComponentC fromA="state.fromA"/>
</div>`;
static components = { ComponentB, ComponentC };
state = useState({ fromA: "a1" });
}
const component = new ComponentA();
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a1<b>a1</b><p><span>a1b1</span></p></div>");
component.state.fromA = "a2";
await nextTick();
expect(fixture.innerHTML).toBe("<div>a1<b>a1</b><p><span>a1b1</span></p></div>");
stateC.fromC = "b2";
await nextTick();
expect(fixture.innerHTML).toBe("<div>a1<b>a1</b><p><span>a1b1</span></p></div>");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div>a2<b>a2</b><p><span>a2b2</span></p></div>");
});
test("concurrent renderings scenario 10", async () => {
// Here is the global idea of this scenario:
// A
// |
// B <- async willUpdateProps
// ----- <- conditional (initialy false)
// |
// C <- async willStart
// Render A and B normally
// Change the condition on B to trigger a re-rendering with C (async willStart)
// Change the state on A to trigger a global re-rendering, which is blocked
// in B (async willUpdateProps)
// Resolve the willStart of C: the first re-rendering has been cancelled by
// the global re-rendering, but handlers waiting for the rendering promise to
// resolve might execute and we don't want them to crash/do anything
const defB = makeDeferred();
const defC = makeDeferred();
let stateB;
class ComponentC extends Component {
static template = xml`<span><t t-esc="props.value"/></span>`;
willStart() {
return defC;
}
}
ComponentC.prototype.__render = jest.fn(ComponentC.prototype.__render);
class ComponentB extends Component {
static template = xml`<p><ComponentC t-if="state.hasChild" value="props.value"/></p>`;
state = useState({ hasChild: false });
static components = { ComponentC };
constructor(parent, props) {
super(parent, props);
stateB = this.state;
}
willUpdateProps() {
return defB;
}
}
class ComponentA extends Component {
static template = xml`<div><ComponentB value="state.value"/></div>`;
static components = { ComponentB };
state = useState({ value: 1 });
}
const componentA = new ComponentA();
await componentA.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p></p></div>");
stateB.hasChild = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p></p></div>");
componentA.state.value = 2;
defC.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p></p></div>");
defB.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span>2</span></p></div>");
expect(ComponentC.prototype.__render).toHaveBeenCalledTimes(1);
});
test("concurrent renderings scenario 11", async () => {
// This scenario is the following: we have a component being updated (by props),
// and then rendered (render method), but before the willUpdateProps resolves.
// We check that in that case, the return value of the render method is a promise
// that is resolved when the component is completely rendered (so, properly
// remapped to the promise of the ambient rendering)
const def = makeDeferred();
let child;
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/>|<t t-esc="val"/></span>`;
val = 3;
willUpdateProps() {
child = this;
return def;
}
}
class Parent extends Component {
static template = xml`<div><Child val="state.valA"/></div>`;
static components = { Child };
state = useState({ valA: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1|3</span></div>");
parent.state.valA = 2;
await nextTick();
setTimeout(() => {
def.resolve();
}, 20);
child.val = 5;
await child.render();
expect(fixture.innerHTML).toBe("<div><span>2|5</span></div>");
});
test("concurrent renderings scenario 12", async () => {
// In this scenario, we have a parent component that will be re-rendered
// several times simultaneously:
// - once in a tick: it will create a new fiber, render it, but will have
// to wait for its child (blocking) to be completed
// - twice in the next tick: it will twice reuse the same fiber (as it is
// rendered but not completed yet)
const def = makeDeferred();
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
willUpdateProps() {
return def;
}
}
class Parent extends Component {
static template = xml`<div><Child val="state.val"/></div>`;
static components = { Child };
state = useState({ val: 1 });
}
Parent.prototype.__render = jest.fn(Parent.prototype.__render);
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(1);
parent.state.val = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(2);
parent.state.val = 3;
parent.state.val = 4;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(3);
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>4</span></div>");
expect(Parent.prototype.__render).toHaveBeenCalledTimes(3);
});
test("concurrent renderings scenario 13", async () => {
let lastChild;
class Child extends Component {
static template = xml`<span><t t-esc="state.val"/></span>`;
state = useState({ val: 0 });
mounted() {
if (lastChild) {
lastChild.state.val = 0;
}
lastChild = this;
this.state.val = 1;
}
}
class Parent extends Component {
static template = xml`
<div>
<Child/>
<Child t-if="state.bool"/>
</div>`;
static components = { Child };
state = useState({ bool: false });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>0</span></div>");
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.state.bool = true;
await nextTick(); // wait for this change to be applied
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("<div><span>0</span><span>1</span></div>");
});
test("concurrent renderings scenario 14", async () => {
let b: B | undefined = undefined;
let c: C | undefined = undefined;
class C extends Component {
static template = xml`
<p>
<span t-esc="props.fromA"/>
<span t-esc="props.fromB"/>
<span t-esc="state.fromC"/>
</p>`;
state = useState({ fromC: 3 });
constructor(parent, props) {
super(parent, props);
c = this;
}
}
class B extends Component {
static template = xml`<p><C fromB="state.fromB" fromA="props.fromA"/></p>`;
static components = { C };
constructor(parent, props) {
super(parent, props);
b = this;
}
state = useState({ fromB: 2 });
}
class A extends Component {
static template = xml`<p><B fromA="state.fromA"/></p>`;
static components = { B };
state = useState({ fromA: 1 });
}
const a = new A();
await a.mount(fixture);
expect(fixture.innerHTML).toBe(
"<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>"
);
// trigger a re-rendering of the whole tree
a.state.fromA += 10;
// wait enough for the whole tree to be re-rendered, but not patched yet
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect(fixture.innerHTML).toBe(
"<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>"
);
// trigger a re-rendering from C, which will remap its new fiber
c!.state.fromC += 10;
// trigger a re-rendering from B, which will remap its new fiber as well
b!.state.fromB += 10;
await nextTick();
// at this point, all re-renderings should have been done correctly, and
// the root fiber (A) counter should have been reset to 0, so the DOM should
// have been patched with the updated version of each component
expect(fixture.innerHTML).toBe(
"<p><p><p><span>11</span><span>12</span><span>13</span></p></p></p>"
);
});
test("concurrent renderings scenario 15", async () => {
let b: B | undefined = undefined;
let c: C | undefined = undefined;
class C extends Component {
static template = xml`
<p>
<span t-esc="props.fromA"/>
<span t-esc="props.fromB"/>
<span t-esc="state.fromC"/>
</p>`;
state = useState({ fromC: 3 });
constructor(parent, props) {
super(parent, props);
c = this;
}
}
class B extends Component {
static template = xml`<p><C fromB="state.fromB" fromA="props.fromA"/></p>`;
static components = { C };
constructor(parent, props) {
super(parent, props);
b = this;
}
state = useState({ fromB: 2 });
}
class A extends Component {
static template = xml`<p><B fromA="state.fromA"/></p>`;
static components = { B };
state = useState({ fromA: 1 });
}
const a = new A();
await a.mount(fixture);
expect(fixture.innerHTML).toBe(
"<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>"
);
// trigger a re-rendering of the whole tree
a.state.fromA += 10;
// wait enough for the whole tree to be re-rendered, but not patched yet
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect(fixture.innerHTML).toBe(
"<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>"
);
// trigger a re-rendering from C, which will remap its new fiber
c!.state.fromC += 10;
// trigger a re-rendering from B, which will remap its new fiber as well
b!.state.fromB += 10;
// simulate a flush (nothing should have changed as no fiber should have its
// counter to 0)
Component.scheduler.flush();
expect(fixture.innerHTML).toBe(
"<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>"
);
// wait a bit and simulate another flush (we expect nothing to change as well)
await nextMicroTick();
Component.scheduler.flush();
expect(fixture.innerHTML).toBe(
"<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>"
);
await nextTick();
expect(fixture.innerHTML).toBe(
"<p><p><p><span>11</span><span>12</span><span>13</span></p></p></p>"
);
});
test("concurrent renderings scenario 16", async () => {
expect.assertions(4);
let b: B | undefined = undefined;
let c: C | undefined = undefined;
class D extends Component {
static template = xml`<ul>DDD</ul>`;
async willStart() {
await nextTick();
await nextTick();
}
}
class C extends Component {
static template = xml`
<p>
<span t-esc="props.fromA"/>
<span t-esc="props.fromB"/>
<span t-esc="state.fromC"/>
<D t-if="state.fromC === 13"/>
</p>`;
static components = { D };
state = { fromC: 3 }; // not reactive
constructor(parent, props) {
super(parent, props);
c = this;
}
}
class B extends Component {
static template = xml`<p><C fromB="state.fromB" fromA="props.fromA"/></p>`;
static components = { C };
constructor(parent, props) {
super(parent, props);
b = this;
}
state = useState({ fromB: 2 });
}
class A extends Component {
static template = xml`<p><B fromA="state.fromA"/></p>`;
static components = { B };
state = useState({ fromA: 1 });
}
const a = new A();
await a.mount(fixture);
expect(fixture.innerHTML).toBe(
"<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>"
);
// trigger a re-rendering of the whole tree
a.state.fromA += 10;
// wait enough for the whole tree to be re-rendered, but not patched yet
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect(fixture.innerHTML).toBe(
"<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>"
);
// trigger a re-rendering from C, which will remap its new fiber
c!.state.fromC += 10;
const prom = c!.render().then(() => {
expect(fixture.innerHTML).toBe(
"<p><p><p><span>11</span><span>12</span><span>13</span><ul>DDD</ul></p></p></p>"
);
});
// trigger a re-rendering from B, which will remap its new fiber as well
b!.state.fromB += 10;
await nextTick();
// at this point, C rendering is still pending, and nothing should have been
// updated yet.
expect(fixture.innerHTML).toBe(
"<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>"
);
await prom;
});
test("concurrent renderings scenario 17", async () => {
class Parent extends Component {
static template = xml`<span><t t-esc="state.value"/></span>`;
state = useState({ value: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>1</span>");
parent.state.value = 2;
parent.__owl__.currentFiber!.cancel();
parent.state.value = 3; // update value directly
await nextTick();
expect(fixture.innerHTML).toBe("<span>3</span>");
parent.state.value = 4; // update value after a tick
await nextTick();
expect(fixture.innerHTML).toBe("<span>4</span>");
});
test("change state and call manually render: no unnecessary rendering", async () => {
class Widget extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
Widget.prototype.__render = jest.fn(Widget.prototype.__render);
const widget = new Widget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
expect(Widget.prototype.__render).toHaveBeenCalledTimes(1);
widget.state.val = 2;
await widget.render();
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(Widget.prototype.__render).toHaveBeenCalledTimes(2);
});
});