mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
1409 lines
44 KiB
TypeScript
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);
|
|
});
|
|
});
|