mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
64db7777dd
This is a tricky commit. The key point is that the Fiber.complete method, which commits a rendering to the DOM works like this: it traverses the component tree, patch the corresponding DOM for each component, calls the mounted/destroy hooks, and reset the currentfiber of components to null, all synchronously. However, this means that it is possible for components to initiate a rendering (which create a new currentFiber) before the currentFiber is reset to null, so the internal state of owl is corrupted. This can occurs in a crash, as in the test that accompanies this commit. To fix this, we take care of resetting the currentFiber first, while we walk the component tree. Then, the internal state is always consistent (i.e. a currentFiber to null means that there is no pending rendering) closes #904
1621 lines
49 KiB
TypeScript
1621 lines
49 KiB
TypeScript
import { Component, Env, STATUS } 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();
|
|
expect(w.__owl__.status).toBe(STATUS.CREATED);
|
|
w.mount(fixture);
|
|
expect(w.__owl__.status).toBe(STATUS.WILLSTARTED);
|
|
w.destroy();
|
|
expect(w.__owl__.status).toBe(STATUS.DESTROYED);
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(w.__owl__.status).toBe(STATUS.DESTROYED);
|
|
});
|
|
|
|
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("calling render in destroy", async () => {
|
|
let a: any = null;
|
|
let c: any = null;
|
|
|
|
class C extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-esc="props.fromA"/>
|
|
</div>`;
|
|
}
|
|
|
|
let flag = false;
|
|
class B extends Component {
|
|
static template = xml`<C fromA="props.fromA"/>`;
|
|
static components = { C };
|
|
|
|
setup() {
|
|
c = this;
|
|
}
|
|
|
|
mounted() {
|
|
if (flag) {
|
|
this.render();
|
|
} else {
|
|
flag = true;
|
|
}
|
|
}
|
|
willUnmount() {
|
|
c.render();
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`<B t-key="key" fromA="state"/>`;
|
|
static components = { B };
|
|
state = "a";
|
|
key = 1;
|
|
|
|
setup() {
|
|
a = this;
|
|
}
|
|
}
|
|
|
|
const parent = new A();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>a</div>");
|
|
|
|
a.state = "A";
|
|
a.key = 2;
|
|
await a.render();
|
|
// this nextTick is critical, otherwise jest may silently swallow errors
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("<div>A</div>");
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
test("components with shouldUpdate=false", async () => {
|
|
const state = { p: 1, cc: 10 };
|
|
|
|
class ChildChild extends Component {
|
|
static template = xml`
|
|
<div>
|
|
child child: <t t-esc="state.cc"/>
|
|
</div>`;
|
|
state = state;
|
|
shouldUpdate() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class Child extends Component {
|
|
static components = { ChildChild };
|
|
static template = xml`
|
|
<div>
|
|
child
|
|
<ChildChild/>
|
|
</div>`;
|
|
|
|
shouldUpdate() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
let parent: any;
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
parent: <t t-esc="state.p"/>
|
|
<Child/>
|
|
</div>`;
|
|
|
|
state = state;
|
|
constructor(a, b) {
|
|
super(a, b);
|
|
parent = this;
|
|
}
|
|
shouldUpdate() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class App extends Component {
|
|
static components = { Parent };
|
|
static template = xml`
|
|
<div>
|
|
<Parent/>
|
|
</div>`;
|
|
}
|
|
|
|
var div = document.createElement("div");
|
|
fixture.appendChild(div);
|
|
|
|
const app = new App();
|
|
|
|
await app.mount(fixture);
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div></div><div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
|
|
);
|
|
app.mount(div);
|
|
|
|
// wait for rendering from second mount to go through parent
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
state.cc++;
|
|
state.p++;
|
|
parent.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div><div> parent: 2<div> child <div> child child: 11</div></div></div></div></div>"
|
|
);
|
|
});
|
|
|
|
test("components with shouldUpdate=false, part 2", async () => {
|
|
const state = { p: 1, cc: 10 };
|
|
let shouldUpdate = true;
|
|
|
|
class ChildChild extends Component {
|
|
static template = xml`
|
|
<div>
|
|
child child: <t t-esc="state.cc"/>
|
|
</div>`;
|
|
state = state;
|
|
shouldUpdate() {
|
|
return shouldUpdate;
|
|
}
|
|
}
|
|
|
|
class Child extends Component {
|
|
static components = { ChildChild };
|
|
static template = xml`
|
|
<div>
|
|
child
|
|
<ChildChild/>
|
|
</div>`;
|
|
|
|
shouldUpdate() {
|
|
return shouldUpdate;
|
|
}
|
|
}
|
|
|
|
let parent: any;
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
parent: <t t-esc="state.p"/>
|
|
<Child/>
|
|
</div>`;
|
|
|
|
state = state;
|
|
constructor(a, b) {
|
|
super(a, b);
|
|
parent = this;
|
|
}
|
|
shouldUpdate() {
|
|
return shouldUpdate;
|
|
}
|
|
}
|
|
|
|
class App extends Component {
|
|
static components = { Parent };
|
|
static template = xml`
|
|
<div>
|
|
<Parent/>
|
|
</div>`;
|
|
}
|
|
|
|
const app = new App();
|
|
|
|
await app.mount(fixture);
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
|
|
);
|
|
|
|
state.cc++;
|
|
state.p++;
|
|
app.render();
|
|
|
|
// wait for rendering to go through child
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
|
|
shouldUpdate = false;
|
|
parent.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div> parent: 2<div> child <div> child child: 11</div></div></div></div>"
|
|
);
|
|
});
|
|
});
|