mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
0048205636
This allows them to be updated automatically when needed instead of having to update them by hand.
5007 lines
122 KiB
TypeScript
5007 lines
122 KiB
TypeScript
import {
|
|
App,
|
|
Component,
|
|
ComponentConstructor,
|
|
mount,
|
|
onMounted,
|
|
onRendered,
|
|
onWillDestroy,
|
|
onWillStart,
|
|
onWillUnmount,
|
|
onWillUpdateProps,
|
|
useState,
|
|
xml,
|
|
} from "../../src";
|
|
import { Fiber } from "../../src/runtime/fibers";
|
|
import { Scheduler } from "../../src/runtime/scheduler";
|
|
import { status } from "../../src/runtime/status";
|
|
import {
|
|
makeDeferred,
|
|
makeTestFixture,
|
|
nextMicroTick,
|
|
nextTick,
|
|
snapshotEverything,
|
|
steps,
|
|
useLogLifecycle,
|
|
} from "../helpers";
|
|
|
|
let fixture: HTMLElement;
|
|
|
|
snapshotEverything();
|
|
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
});
|
|
|
|
// following code is there to prevent memory leaks in the scheduled tasks
|
|
let lastScheduler: Scheduler;
|
|
const addFiber = Scheduler.prototype.addFiber;
|
|
Scheduler.prototype.addFiber = function (fiber: Fiber) {
|
|
lastScheduler = this;
|
|
return addFiber.call(this, fiber);
|
|
};
|
|
|
|
afterEach(() => {
|
|
if (lastScheduler && lastScheduler.tasks.size > 0) {
|
|
// we still clear the scheduler to prevent additional noise
|
|
lastScheduler.tasks.clear();
|
|
throw new Error("we got a memory leak...");
|
|
}
|
|
});
|
|
|
|
describe("async rendering", () => {
|
|
test("destroying a widget before start is over", async () => {
|
|
let def = makeDeferred();
|
|
let w: any = null;
|
|
class W extends Component {
|
|
static template = xml`<div/>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
expect(status(this)).toBe("new");
|
|
w = this;
|
|
onWillStart(() => def);
|
|
}
|
|
}
|
|
const app = new App(W);
|
|
app.mount(fixture);
|
|
expect(status(w)).toBe("new");
|
|
app.destroy();
|
|
expect(status(w)).toBe("destroyed");
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(status(w)).toBe("destroyed");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"W:setup",
|
|
"W:willStart",
|
|
"W:willDestroy",
|
|
]
|
|
`);
|
|
});
|
|
});
|
|
|
|
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>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
n++;
|
|
onWillStart(() => 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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const w = await mount(W, fixture);
|
|
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"W:setup",
|
|
"W:willStart",
|
|
"W:willRender",
|
|
"W:rendered",
|
|
"W:mounted",
|
|
]
|
|
`);
|
|
|
|
expect(n).toBe(0);
|
|
|
|
w.state.val = 2;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"W:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"W:rendered",
|
|
]
|
|
`);
|
|
|
|
w.state.val = 3;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"W:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"W:rendered",
|
|
]
|
|
`);
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>child:3</span></div>");
|
|
expect(Object.values(w.__owl__.children).length).toBe(1);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:willDestroy",
|
|
"W:willPatch",
|
|
"Child:mounted",
|
|
"W:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("destroying/recreating a subcomponent, other scenario", async () => {
|
|
let flag = false;
|
|
|
|
class Child extends Component {
|
|
static template = xml`child`;
|
|
setup() {
|
|
if (!flag) {
|
|
flag = true;
|
|
parent.render(true);
|
|
}
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`parent<Child t-if="state.hasChild"/>`;
|
|
static components = { Child };
|
|
state = useState({ hasChild: false });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("parent");
|
|
|
|
parent.state.hasChild = true;
|
|
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:willDestroy",
|
|
"Parent:willPatch",
|
|
"Child:mounted",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
|
|
expect(fixture.innerHTML).toBe("parentchild");
|
|
});
|
|
|
|
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>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillStart(() => defA);
|
|
}
|
|
|
|
getValue() {
|
|
nbRenderings++;
|
|
return "a";
|
|
}
|
|
}
|
|
|
|
class ChildB extends Component {
|
|
static template = xml`<span>b</span>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillStart(() => defB);
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-if="state.flagA"><ChildA /></t>
|
|
<t t-if="state.flagB"><ChildB /></t>`;
|
|
|
|
static components = { ChildA, ChildB };
|
|
state = useState({ flagA: false, flagB: false });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("");
|
|
|
|
parent.state.flagA = true;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:setup",
|
|
"ChildA:willStart",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("");
|
|
|
|
parent.state.flagB = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:setup",
|
|
"ChildA:willStart",
|
|
"ChildB:setup",
|
|
"ChildB:willStart",
|
|
"Parent:rendered",
|
|
"ChildA:willDestroy",
|
|
]
|
|
`);
|
|
|
|
defB.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("");
|
|
expect(nbRenderings).toBe(0);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ChildB:willRender",
|
|
"ChildB:rendered",
|
|
]
|
|
`);
|
|
|
|
defA.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<span>a</span><span>b</span>");
|
|
expect(nbRenderings).toBe(1);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
"Parent:willPatch",
|
|
"ChildB:mounted",
|
|
"ChildA:mounted",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => defA);
|
|
}
|
|
}
|
|
|
|
class ChildB extends Component {
|
|
static template = xml`<span>b<t t-esc="props.val"/></span>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillStart(() => 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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"ChildA:setup",
|
|
"ChildA:willStart",
|
|
"Parent:rendered",
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
"ChildA:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
|
|
|
|
parent.state.valA = 2;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
|
|
|
|
parent.state.flagB = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:willUpdateProps",
|
|
"ChildB:setup",
|
|
"ChildB:willStart",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
defB.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ChildB:willRender",
|
|
"ChildB:rendered",
|
|
]
|
|
`);
|
|
|
|
defA.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>a2</span><span>b2</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
"Parent:willPatch",
|
|
"ChildA:willPatch",
|
|
"ChildB:mounted",
|
|
"ChildA:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => defA);
|
|
}
|
|
}
|
|
class ChildB extends Component {
|
|
static template = xml`<span>b<t t-esc="props.val"/></span>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillStart(() => 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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"ChildA:setup",
|
|
"ChildA:willStart",
|
|
"Parent:rendered",
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
"ChildA:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
|
|
|
|
parent.state.valA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
parent.state.flagB = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:willUpdateProps",
|
|
"ChildB:setup",
|
|
"ChildB:willStart",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
defB.resolve();
|
|
expect(fixture.innerHTML).toBe("<div><span>a1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
defA.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>a2</span><span>b2</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ChildB:willRender",
|
|
"ChildB:rendered",
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
"Parent:willPatch",
|
|
"ChildA:willPatch",
|
|
"ChildB:mounted",
|
|
"ChildA:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("update a sub-component twice in the same frame", async () => {
|
|
const defs = [makeDeferred(), makeDeferred()];
|
|
let index = 0;
|
|
class ChildA extends Component {
|
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
|
setup() {
|
|
onWillUpdateProps(() => defs[index++]);
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><ChildA val="state.valA"/></div>`;
|
|
static components = { ChildA };
|
|
state = useState({ valA: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"ChildA:setup",
|
|
"ChildA:willStart",
|
|
"Parent:rendered",
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
"ChildA:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
parent.state.valA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
parent.state.valA = 3;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
defs[0].resolve();
|
|
await Promise.resolve();
|
|
defs[1].resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
"Parent:willPatch",
|
|
"ChildA:willPatch",
|
|
"ChildA:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("update a sub-component twice in the same frame, 2", async () => {
|
|
class ChildA extends Component {
|
|
static template = xml`<span><t t-esc="val()"/></span>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
|
|
val() {
|
|
return this.props.val;
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><ChildA val="state.valA"/></div>`;
|
|
static components = { ChildA };
|
|
state = useState({ valA: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"ChildA:setup",
|
|
"ChildA:willStart",
|
|
"Parent:rendered",
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
"ChildA:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
|
|
parent.state.valA = 2;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
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.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
parent.state.valA = 3;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"ChildA:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextMicroTick();
|
|
// same as above
|
|
await nextMicroTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ChildA:willRender",
|
|
"ChildA:rendered",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willPatch",
|
|
"ChildA:willPatch",
|
|
"ChildA:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("properly behave when destroyed/unmounted while rendering ", async () => {
|
|
const def = makeDeferred();
|
|
|
|
class SubChild extends Component {
|
|
static template = xml`<div/>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => {
|
|
return def;
|
|
});
|
|
}
|
|
}
|
|
|
|
class Child extends Component {
|
|
static template = xml`<div><SubChild val="props.val"/></div>`;
|
|
static components = { SubChild };
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
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" });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div><div></div></div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"SubChild:setup",
|
|
"SubChild:willStart",
|
|
"Child:rendered",
|
|
"SubChild:willRender",
|
|
"SubChild:rendered",
|
|
"SubChild:mounted",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
// 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>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"SubChild:willUpdateProps",
|
|
"Child:rendered",
|
|
]
|
|
`);
|
|
|
|
// 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>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Parent:willPatch",
|
|
"Child:willUnmount",
|
|
"SubChild:willUnmount",
|
|
"SubChild:willDestroy",
|
|
"Child:willDestroy",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
|
|
// we now resolve def, so the child rendering is now complete.
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
});
|
|
|
|
test("rendering component again in next microtick", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<div>Child</div>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<button t-on-click="onClick">Click</button>
|
|
<t t-if="env.config.flag"><Child/></t>
|
|
</div>`;
|
|
static components = { Child };
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
async onClick() {
|
|
this.env.config.flag = true;
|
|
this.render();
|
|
await Promise.resolve();
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
const env = { config: { flag: false } };
|
|
await mount(Parent, fixture, { env });
|
|
expect(fixture.innerHTML).toBe("<div><button>Click</button></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
fixture.querySelector("button")!.click();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><button>Click</button><div>Child</div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:willDestroy",
|
|
"Parent:willPatch",
|
|
"Child:mounted",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("concurrent renderings scenario 1", async () => {
|
|
const def = makeDeferred();
|
|
let stateB: any = null;
|
|
|
|
class ComponentC extends Component {
|
|
static template = xml`<span><t t-esc="props.fromA"/><t t-esc="someValue()"/></span>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => def);
|
|
}
|
|
someValue() {
|
|
return this.props.fromB;
|
|
}
|
|
}
|
|
ComponentC.prototype.someValue = jest.fn(ComponentC.prototype.someValue);
|
|
|
|
class ComponentB extends Component {
|
|
static template = xml`<p><ComponentC fromA="props.fromA" fromB="state.fromB" /></p>`;
|
|
static components = { ComponentC };
|
|
state = useState({ fromB: "b" });
|
|
|
|
setup() {
|
|
stateB = this.state;
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class ComponentA extends Component {
|
|
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
|
|
static components = { ComponentB };
|
|
state = useState({ fromA: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentC:setup",
|
|
"ComponentC:willStart",
|
|
"ComponentB:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentC:rendered",
|
|
"ComponentC:mounted",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
stateB.fromB = "c";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentC:willUpdateProps",
|
|
"ComponentB:rendered",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentC:willUpdateProps",
|
|
"ComponentB:rendered",
|
|
]
|
|
`);
|
|
expect(ComponentC.prototype.someValue).toBeCalledTimes(1);
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span>2c</span></p></div>");
|
|
expect(ComponentC.prototype.someValue).toBeCalledTimes(2);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentC:willRender",
|
|
"ComponentC:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentC:willPatch",
|
|
"ComponentC:patched",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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: any = null;
|
|
class ComponentC extends Component {
|
|
static template = xml`<span><t t-esc="props.fromA"/><t t-esc="props.fromB"/></span>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => 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" });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>1<p><span>1b</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentC:setup",
|
|
"ComponentC:willStart",
|
|
"ComponentB:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentC:rendered",
|
|
"ComponentC:mounted",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>1<p><span>1b</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentC:willUpdateProps",
|
|
"ComponentB:rendered",
|
|
]
|
|
`);
|
|
|
|
stateB.fromB = "c";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>1<p><span>1b</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentC:willUpdateProps",
|
|
"ComponentB:rendered",
|
|
]
|
|
`);
|
|
|
|
defs[1].resolve(); // resolve rendering initiated in B
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>2<p><span>2c</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentC:willRender",
|
|
"ComponentC:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentC:willPatch",
|
|
"ComponentC:patched",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
|
|
defs[0].resolve(); // resolve rendering initiated in A
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>2<p><span>2c</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
});
|
|
|
|
test("concurrent renderings scenario 2bis", async () => {
|
|
const defs = [makeDeferred(), makeDeferred()];
|
|
let index = 0;
|
|
let stateB: any = null;
|
|
class ComponentC extends Component {
|
|
static template = xml`<span><t t-esc="props.fromA"/><t t-esc="props.fromB"/></span>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => 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" });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
stateB = this.state;
|
|
}
|
|
}
|
|
|
|
class ComponentA extends Component {
|
|
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
|
|
static components = { ComponentB };
|
|
state = useState({ fromA: 1 });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentC:setup",
|
|
"ComponentC:willStart",
|
|
"ComponentB:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentC:rendered",
|
|
"ComponentC:mounted",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentC:willUpdateProps",
|
|
"ComponentB:rendered",
|
|
]
|
|
`);
|
|
|
|
stateB.fromB = "c";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span>1b</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentC:willUpdateProps",
|
|
"ComponentB:rendered",
|
|
]
|
|
`);
|
|
|
|
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
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
defs[1].resolve(); // resolve rendering initiated in B
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span>2c</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentC:willRender",
|
|
"ComponentC:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentC:willPatch",
|
|
"ComponentC:patched",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("concurrent renderings scenario 3", async () => {
|
|
const defB = makeDeferred();
|
|
const defsD = [makeDeferred(), makeDeferred()];
|
|
let index = 0;
|
|
let stateC: any = null;
|
|
|
|
class ComponentD extends Component {
|
|
static template = xml`<i><t t-esc="props.fromA"/><t t-esc="someValue()"/></i>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => defsD[index++]);
|
|
}
|
|
someValue() {
|
|
return this.props.fromC;
|
|
}
|
|
}
|
|
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" });
|
|
setup() {
|
|
useLogLifecycle();
|
|
stateC = this.state;
|
|
}
|
|
}
|
|
|
|
class ComponentB extends Component {
|
|
static template = xml`<p><ComponentC fromA="props.fromA" /></p>`;
|
|
static components = { ComponentC };
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => defB);
|
|
}
|
|
}
|
|
|
|
class ComponentA extends Component {
|
|
static components = { ComponentB };
|
|
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
|
|
state = useState({ fromA: 1 });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentC:setup",
|
|
"ComponentC:willStart",
|
|
"ComponentB:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentD:setup",
|
|
"ComponentD:willStart",
|
|
"ComponentC:rendered",
|
|
"ComponentD:willRender",
|
|
"ComponentD:rendered",
|
|
"ComponentD:mounted",
|
|
"ComponentC:mounted",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
]
|
|
`);
|
|
|
|
stateC.fromC = "d";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
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>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentC:willUpdateProps",
|
|
"ComponentB:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentD:willUpdateProps",
|
|
"ComponentC:rendered",
|
|
]
|
|
`);
|
|
|
|
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(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentD:willRender",
|
|
"ComponentD:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentC:willPatch",
|
|
"ComponentD:willPatch",
|
|
"ComponentD:patched",
|
|
"ComponentC:patched",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
|
|
});
|
|
|
|
test("concurrent renderings scenario 4", async () => {
|
|
const defB = makeDeferred();
|
|
const defsD = [makeDeferred(), makeDeferred()];
|
|
let index = 0;
|
|
let stateC: any = null;
|
|
|
|
class ComponentD extends Component {
|
|
static template = xml`<i><t t-esc="props.fromA"/><t t-esc="someValue()"/></i>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => defsD[index++]);
|
|
}
|
|
someValue() {
|
|
return this.props.fromC;
|
|
}
|
|
}
|
|
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" });
|
|
setup() {
|
|
useLogLifecycle();
|
|
stateC = this.state;
|
|
}
|
|
}
|
|
|
|
class ComponentB extends Component {
|
|
static template = xml`<p><ComponentC fromA="props.fromA" /></p>`;
|
|
static components = { ComponentC };
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => defB);
|
|
}
|
|
}
|
|
|
|
class ComponentA extends Component {
|
|
static components = { ComponentB };
|
|
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
|
|
state = useState({ fromA: 1 });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentC:setup",
|
|
"ComponentC:willStart",
|
|
"ComponentB:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentD:setup",
|
|
"ComponentD:willStart",
|
|
"ComponentC:rendered",
|
|
"ComponentD:willRender",
|
|
"ComponentD:rendered",
|
|
"ComponentD:mounted",
|
|
"ComponentC:mounted",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
]
|
|
`);
|
|
|
|
stateC.fromC = "d";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
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>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentC:willUpdateProps",
|
|
"ComponentB:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentD:willUpdateProps",
|
|
"ComponentC:rendered",
|
|
]
|
|
`);
|
|
|
|
defsD[1].resolve(); // completely resolve rendering initiated in A
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
|
|
expect(ComponentD.prototype.someValue).toBeCalledTimes(1);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
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);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentD:willRender",
|
|
"ComponentD:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentC:willPatch",
|
|
"ComponentD:willPatch",
|
|
"ComponentD:patched",
|
|
"ComponentC:patched",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => defsB[index++]);
|
|
}
|
|
someValue() {
|
|
return this.props.fromA;
|
|
}
|
|
}
|
|
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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 3;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
]
|
|
`);
|
|
|
|
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);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
defsB[1].resolve(); // resolve second re-rendering
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
|
|
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => defsB[index++]);
|
|
}
|
|
someValue() {
|
|
return this.props.fromA;
|
|
}
|
|
}
|
|
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 });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 3;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
]
|
|
`);
|
|
|
|
defsB[1].resolve(); // resolve second re-rendering
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
|
|
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
|
|
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);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
});
|
|
|
|
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" });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => {
|
|
this.state.fromB = "c";
|
|
});
|
|
}
|
|
someValue() {
|
|
return this.state.fromB;
|
|
}
|
|
}
|
|
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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>1b</p></div>");
|
|
expect(ComponentB.prototype.someValue).toBeCalledTimes(1);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>2c</p></div>");
|
|
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("concurrent renderings scenario 8", async () => {
|
|
const def = makeDeferred();
|
|
let stateB: any = null;
|
|
class ComponentB extends Component {
|
|
static template = xml`<p><t t-esc="props.fromA" /><t t-esc="state.fromB" /></p>`;
|
|
state = useState({ fromB: "b" });
|
|
setup() {
|
|
useLogLifecycle();
|
|
stateB = this.state;
|
|
onWillUpdateProps(() => def);
|
|
}
|
|
}
|
|
|
|
class ComponentA extends Component {
|
|
static components = { ComponentB };
|
|
static template = xml`<div><ComponentB fromA="state.fromA"/></div>`;
|
|
state = useState({ fromA: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>1b</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>1b</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
]
|
|
`);
|
|
|
|
stateB.fromB = "c";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>1b</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>2c</p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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: any = null;
|
|
|
|
class ComponentD extends Component {
|
|
static template = xml`<span><t t-esc="props.fromA"/><t t-esc="props.fromC"/></span>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class ComponentC extends Component {
|
|
static template = xml`<p><ComponentD fromA="props.fromA" fromC="state.fromC" /></p>`;
|
|
static components = { ComponentD };
|
|
state = useState({ fromC: "b1" });
|
|
|
|
setup() {
|
|
stateC = this.state;
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
class ComponentB extends Component {
|
|
static template = xml`<b><t t-esc="props.fromA"/></b>`;
|
|
|
|
setup() {
|
|
onWillUpdateProps(() => def);
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
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" });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const component = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>a1<b>a1</b><p><span>a1b1</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentC:setup",
|
|
"ComponentC:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentD:setup",
|
|
"ComponentD:willStart",
|
|
"ComponentC:rendered",
|
|
"ComponentD:willRender",
|
|
"ComponentD:rendered",
|
|
"ComponentD:mounted",
|
|
"ComponentC:mounted",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
component.state.fromA = "a2";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>a1<b>a1</b><p><span>a1b1</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentC:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentD:willUpdateProps",
|
|
"ComponentC:rendered",
|
|
"ComponentD:willRender",
|
|
"ComponentD:rendered",
|
|
]
|
|
`);
|
|
|
|
stateC.fromC = "b2";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>a1<b>a1</b><p><span>a1b1</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentC:willRender",
|
|
"ComponentD:willUpdateProps",
|
|
"ComponentC:rendered",
|
|
"ComponentD:willRender",
|
|
"ComponentD:rendered",
|
|
]
|
|
`);
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>a2<b>a2</b><p><span>a2b2</span></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentC:willPatch",
|
|
"ComponentD:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentB:patched",
|
|
"ComponentD:patched",
|
|
"ComponentC:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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: any = null;
|
|
let rendered = 0;
|
|
class ComponentC extends Component {
|
|
static template = xml`<span><t t-esc="value"/></span>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillStart(() => defC);
|
|
}
|
|
get value() {
|
|
rendered++;
|
|
return this.props.value;
|
|
}
|
|
}
|
|
|
|
class ComponentB extends Component {
|
|
static template = xml`<p><ComponentC t-if="state.hasChild" value="props.value"/></p>`;
|
|
state = useState({ hasChild: false });
|
|
static components = { ComponentC };
|
|
setup() {
|
|
useLogLifecycle();
|
|
stateB = this.state;
|
|
onWillUpdateProps(() => defB);
|
|
}
|
|
}
|
|
|
|
class ComponentA extends Component {
|
|
static template = xml`<div><ComponentB value="state.value"/></div>`;
|
|
static components = { ComponentB };
|
|
state = useState({ value: 1 });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const componentA = await mount(ComponentA, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:setup",
|
|
"ComponentA:willStart",
|
|
"ComponentA:willRender",
|
|
"ComponentB:setup",
|
|
"ComponentB:willStart",
|
|
"ComponentA:rendered",
|
|
"ComponentB:willRender",
|
|
"ComponentB:rendered",
|
|
"ComponentB:mounted",
|
|
"ComponentA:mounted",
|
|
]
|
|
`);
|
|
|
|
stateB.hasChild = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentC:setup",
|
|
"ComponentC:willStart",
|
|
"ComponentB:rendered",
|
|
]
|
|
`);
|
|
|
|
componentA.state.value = 2;
|
|
defC.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p></p></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentA:willRender",
|
|
"ComponentB:willUpdateProps",
|
|
"ComponentA:rendered",
|
|
"ComponentC:willDestroy",
|
|
]
|
|
`);
|
|
|
|
defB.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p><span>2</span></p></div>");
|
|
expect(rendered).toBe(1);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"ComponentB:willRender",
|
|
"ComponentC:setup",
|
|
"ComponentC:willStart",
|
|
"ComponentB:rendered",
|
|
"ComponentC:willRender",
|
|
"ComponentC:rendered",
|
|
"ComponentA:willPatch",
|
|
"ComponentB:willPatch",
|
|
"ComponentC:mounted",
|
|
"ComponentB:patched",
|
|
"ComponentA:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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: any = null;
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.val"/>|<t t-esc="val"/></span>`;
|
|
val = 3;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => {
|
|
child = this;
|
|
return def;
|
|
});
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child val="state.valA"/></div>`;
|
|
static components = { Child };
|
|
state = useState({ valA: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>1|3</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
parent.state.valA = 2;
|
|
await nextTick();
|
|
setTimeout(() => {
|
|
def.resolve();
|
|
}, 20);
|
|
child.val = 5;
|
|
child.render();
|
|
await def;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>2|5</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Parent:willPatch",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => def);
|
|
}
|
|
}
|
|
|
|
let rendered = 0;
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child val="val"/></div>`;
|
|
static components = { Child };
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
|
|
get val() {
|
|
rendered++;
|
|
return this.state.val;
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
expect(rendered).toBe(1);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
parent.state.val = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
expect(rendered).toBe(2);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
parent.state.val = 3;
|
|
parent.state.val = 4;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
expect(rendered).toBe(3);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>4</span></div>");
|
|
expect(rendered).toBe(3);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Parent:willPatch",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("concurrent renderings scenario 13", async () => {
|
|
let lastChild: any = null;
|
|
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="state.val"/></span>`;
|
|
state = useState({ val: 0 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
onMounted(() => {
|
|
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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>0</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextTick(); // wait for changes triggered in mounted to be applied
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
]
|
|
`);
|
|
|
|
parent.state.bool = true;
|
|
await nextTick(); // wait for this change to be applied
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Parent:willPatch",
|
|
"Child:mounted",
|
|
"Parent:patched",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextTick(); // wait for changes triggered in mounted to be applied
|
|
expect(fixture.innerHTML).toBe("<div><span>0</span><span>1</span></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
c = this;
|
|
}
|
|
}
|
|
class B extends Component {
|
|
static template = xml`<p><C fromB="state.fromB" fromA="props.fromA"/></p>`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
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 });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const a = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// 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>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
]
|
|
`);
|
|
|
|
// 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>"
|
|
);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"C:willPatch",
|
|
"C:patched",
|
|
"B:patched",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
c = this;
|
|
}
|
|
}
|
|
class B extends Component {
|
|
static template = xml`<p><C fromB="state.fromB" fromA="props.fromA"/></p>`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
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 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const app = new App(A);
|
|
const a = await app.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// 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>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
]
|
|
`);
|
|
|
|
// 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)
|
|
app.scheduler.flush();
|
|
expect(fixture.innerHTML).toBe("<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
// wait a bit and simulate another flush (we expect nothing to change as well)
|
|
await nextMicroTick();
|
|
app.scheduler.flush();
|
|
expect(fixture.innerHTML).toBe("<p><p><p><span>1</span><span>2</span><span>3</span></p></p></p>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"C:willRender",
|
|
"C:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
"<p><p><p><span>11</span><span>12</span><span>13</span></p></p></p>"
|
|
);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"C:willPatch",
|
|
"C:patched",
|
|
"B:patched",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("concurrent renderings scenario 16", async () => {
|
|
let b: B | undefined = undefined;
|
|
let c: C | undefined = undefined;
|
|
class D extends Component {
|
|
static template = xml`D`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillStart(async () => {
|
|
await nextTick();
|
|
await nextTick();
|
|
});
|
|
}
|
|
}
|
|
class C extends Component {
|
|
static template = xml`
|
|
<t t-esc="props.fromA"/>:<t t-esc="props.fromB"/>:<t t-esc="state.fromC"/>:
|
|
<D t-if="state.fromC === 13"/>`;
|
|
static components = { D };
|
|
state = { fromC: 3 }; // not reactive
|
|
setup() {
|
|
useLogLifecycle();
|
|
c = this;
|
|
}
|
|
}
|
|
class B extends Component {
|
|
static template = xml`<C fromB="state.fromB" fromA="props.fromA"/>`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
b = this;
|
|
}
|
|
state = { fromB: 2 };
|
|
}
|
|
class A extends Component {
|
|
static template = xml`<B fromA="state.fromA"/>`;
|
|
static components = { B };
|
|
state = useState({ fromA: 1 });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const a = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("1:2:3: ");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// 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();
|
|
await nextMicroTick();
|
|
expect(fixture.innerHTML).toBe("1:2:3: ");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
]
|
|
`);
|
|
|
|
// trigger a re-rendering from C, which will remap its new fiber
|
|
c!.state.fromC += 10;
|
|
c!.render();
|
|
await nextMicroTick();
|
|
// trigger a re-rendering from B, which will remap its new fiber as well
|
|
b!.state.fromB += 10;
|
|
b!.render();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"C:willRender",
|
|
"D:setup",
|
|
"D:willStart",
|
|
"C:rendered",
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"D:setup",
|
|
"D:willStart",
|
|
"C:rendered",
|
|
"D:willDestroy",
|
|
]
|
|
`);
|
|
|
|
// at this point, C rendering is still pending, and nothing should have been
|
|
// updated yet.
|
|
expect(fixture.innerHTML).toBe("1:2:3: ");
|
|
await nextTick();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("11:12:13: D");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"D:willRender",
|
|
"D:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"C:willPatch",
|
|
"D:mounted",
|
|
"C:patched",
|
|
"B:patched",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
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() {
|
|
useLogLifecycle();
|
|
c = this;
|
|
onMounted(() => {
|
|
if (flag) {
|
|
this.render();
|
|
} else {
|
|
flag = true;
|
|
}
|
|
});
|
|
|
|
onWillUnmount(() => {
|
|
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 app = new App(A);
|
|
await app.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>a</div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:setup",
|
|
"B:willStart",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"B:mounted",
|
|
]
|
|
`);
|
|
|
|
a.state = "A";
|
|
a.key = 2;
|
|
a.render();
|
|
await nextTick();
|
|
// this nextTick is critical, otherwise jest may silently swallow errors
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:setup",
|
|
"B:willStart",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"B:willUnmount",
|
|
"B:willDestroy",
|
|
"B:mounted",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"B:willPatch",
|
|
"B:patched",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div>A</div>");
|
|
});
|
|
|
|
test("change state and call manually render: no unnecessary rendering", async () => {
|
|
let numberOfRender = 0;
|
|
|
|
class Test extends Component {
|
|
static template = xml`<div><t t-esc="value"/></div>`;
|
|
state = useState({ val: 1 });
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
get value() {
|
|
numberOfRender++;
|
|
return this.state.val;
|
|
}
|
|
}
|
|
|
|
const test = await mount(Test, fixture);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Test:setup",
|
|
"Test:willStart",
|
|
"Test:willRender",
|
|
"Test:rendered",
|
|
"Test:mounted",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
expect(numberOfRender).toBe(1);
|
|
|
|
test.state.val = 2;
|
|
test.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>2</div>");
|
|
expect(numberOfRender).toBe(2);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Test:willRender",
|
|
"Test:rendered",
|
|
"Test:willPatch",
|
|
"Test:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("changing state before first render does not trigger a render", async () => {
|
|
let renders = 0;
|
|
|
|
class TestW extends Component {
|
|
static template = xml`<div t-esc="value"/>`;
|
|
state = useState({ drinks: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
this.state.drinks++;
|
|
onWillStart(() => {
|
|
this.state.drinks++;
|
|
});
|
|
}
|
|
get value() {
|
|
renders++;
|
|
return this.state.drinks;
|
|
}
|
|
}
|
|
await mount(TestW, fixture);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"TestW:setup",
|
|
"TestW:willStart",
|
|
"TestW:willRender",
|
|
"TestW:rendered",
|
|
"TestW:mounted",
|
|
]
|
|
`);
|
|
|
|
await nextTick();
|
|
expect(renders).toBe(1);
|
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
});
|
|
|
|
test("changing state before first render does not trigger a render (with parent)", async () => {
|
|
let renders = 0;
|
|
|
|
class TestW extends Component {
|
|
static template = xml`<div t-esc="value"/>`;
|
|
state = useState({ drinks: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
this.state.drinks++;
|
|
onWillStart(() => {
|
|
this.state.drinks++;
|
|
});
|
|
}
|
|
get value() {
|
|
renders++;
|
|
return this.state.drinks;
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static components = { TestW };
|
|
static template = xml`<div><TestW t-if="state.flag"/></div>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
state = useState({ flag: false });
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
parent.state.flag = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><div>3</div></div>");
|
|
expect(renders).toBe(1);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"TestW:setup",
|
|
"TestW:willStart",
|
|
"Parent:rendered",
|
|
"TestW:willRender",
|
|
"TestW:rendered",
|
|
"Parent:willPatch",
|
|
"TestW:mounted",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("two renderings initiated between willPatch and patched", async () => {
|
|
let parent: any = null;
|
|
|
|
class Panel extends Component {
|
|
static template = xml`<abc><t t-esc="props.val"/><t t-esc="mounted" /></abc>`;
|
|
mounted: any;
|
|
setup() {
|
|
useLogLifecycle();
|
|
onMounted(() => {
|
|
this.mounted = "Mounted";
|
|
parent.render(true);
|
|
});
|
|
onWillUnmount(() => parent.render(true));
|
|
}
|
|
}
|
|
|
|
// Main root component
|
|
class Parent extends Component {
|
|
static template = xml`<div><Panel t-key="'panel_' + state.panel" val="state.panel" t-if="state.flag"/></div>`;
|
|
static components = { Panel };
|
|
state = useState({ panel: "Panel1", flag: true });
|
|
setup() {
|
|
useLogLifecycle();
|
|
parent = this;
|
|
}
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><abc>Panel1</abc></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Panel:setup",
|
|
"Panel:willStart",
|
|
"Parent:rendered",
|
|
"Panel:willRender",
|
|
"Panel:rendered",
|
|
"Panel:mounted",
|
|
"Parent:mounted",
|
|
"Parent:willRender",
|
|
"Panel:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextMicroTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Panel:willRender",
|
|
"Panel:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willPatch",
|
|
"Panel:willPatch",
|
|
"Panel:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div><abc>Panel1Mounted</abc></div>");
|
|
|
|
parent.state.panel = "Panel2";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><abc>Panel2</abc></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Panel:setup",
|
|
"Panel:willStart",
|
|
"Parent:rendered",
|
|
"Panel:willRender",
|
|
"Panel:rendered",
|
|
"Parent:willPatch",
|
|
"Panel:willUnmount",
|
|
"Panel:willDestroy",
|
|
"Panel:mounted",
|
|
"Parent:patched",
|
|
"Parent:willRender",
|
|
"Panel:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><abc>Panel2Mounted</abc></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Panel:willRender",
|
|
"Panel:rendered",
|
|
"Parent:willPatch",
|
|
"Panel:willPatch",
|
|
"Panel:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
|
|
parent.state.flag = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
"Parent:willPatch",
|
|
"Panel:willUnmount",
|
|
"Panel:willDestroy",
|
|
"Parent:patched",
|
|
"Parent:willRender",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willPatch",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("parent and child rendered at exact same time", async () => {
|
|
let child: any = null;
|
|
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.value"/>`;
|
|
setup() {
|
|
child = this;
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child value="state.value"/>`;
|
|
static components = { Child };
|
|
state = { value: 0 };
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("0");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
parent.state.value = 1;
|
|
parent.render();
|
|
child.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("1");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Parent:willPatch",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("delay willUpdateProps", async () => {
|
|
let promise: any = null;
|
|
let child: any;
|
|
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.value"/>_<t t-esc="state.int" />`;
|
|
state: any;
|
|
setup() {
|
|
useLogLifecycle();
|
|
child = this;
|
|
this.state = useState({ int: 0 });
|
|
onWillUpdateProps(async () => {
|
|
await promise;
|
|
this.state.int++;
|
|
});
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child value="state.value"/>`;
|
|
static components = { Child };
|
|
state = { value: 0 };
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("0_0");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
promise = makeDeferred();
|
|
const prom1 = promise;
|
|
parent.state.value = 1;
|
|
child.render(); // trigger a root rendering first
|
|
parent.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("0_0");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
]
|
|
`);
|
|
|
|
promise = makeDeferred();
|
|
const prom2 = promise;
|
|
parent.state.value = 2;
|
|
parent.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("0_0");
|
|
|
|
prom2.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("2_1");
|
|
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Parent:willPatch",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
|
|
prom1.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("2_2");
|
|
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("delay willUpdateProps with rendering grandchild", async () => {
|
|
// This test is a bit tricky, a Parent and one of his grandchildren render while another of the parent's
|
|
// grandchildren is awaiting its willUpdateProps.
|
|
// Technically RootFibers will be downgraded in ChildFibers, keeping the same container RootFiber.
|
|
// This case happens when Parent and ReaciveChild react together to a change in a reactive state/
|
|
let promise: any = null;
|
|
let child: any;
|
|
let reactiveChild: any;
|
|
|
|
// Delayed willUpdateProps
|
|
class DelayedChild extends Component {
|
|
static template = xml`<t t-esc="props.value"/>_<t t-esc="state.int" />`;
|
|
state: any;
|
|
setup() {
|
|
useLogLifecycle();
|
|
child = this;
|
|
this.state = useState({ int: 0 });
|
|
onWillUpdateProps(async () => {
|
|
await promise;
|
|
this.state.int++;
|
|
});
|
|
}
|
|
}
|
|
|
|
// A sibling of the delayed component, we will trigger a render on it manually
|
|
class ReactiveChild extends Component {
|
|
static template = xml`<div />`;
|
|
setup() {
|
|
reactiveChild = this;
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
// The parent of everybody, we also manually trigger render on it.
|
|
class Parent extends Component {
|
|
static template = xml`<DelayedChild value="props.state.value"/><ReactiveChild />`;
|
|
static components = { DelayedChild, ReactiveChild };
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class GrandParent extends Component {
|
|
static template = xml`<Parent state="state"/>`;
|
|
static components = { Parent };
|
|
state = { value: 0 };
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(GrandParent, fixture);
|
|
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"GrandParent:setup",
|
|
"GrandParent:willStart",
|
|
"GrandParent:willRender",
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"GrandParent:rendered",
|
|
"Parent:willRender",
|
|
"DelayedChild:setup",
|
|
"DelayedChild:willStart",
|
|
"ReactiveChild:setup",
|
|
"ReactiveChild:willStart",
|
|
"Parent:rendered",
|
|
"DelayedChild:willRender",
|
|
"DelayedChild:rendered",
|
|
"ReactiveChild:willRender",
|
|
"ReactiveChild:rendered",
|
|
"ReactiveChild:mounted",
|
|
"DelayedChild:mounted",
|
|
"Parent:mounted",
|
|
"GrandParent:mounted",
|
|
]
|
|
`);
|
|
|
|
promise = makeDeferred();
|
|
const prom1 = promise;
|
|
parent.state.value = 1;
|
|
child.render(); // trigger a root rendering first
|
|
parent.render(true);
|
|
reactiveChild.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"GrandParent:willRender",
|
|
"Parent:willUpdateProps",
|
|
"GrandParent:rendered",
|
|
"Parent:willRender",
|
|
"DelayedChild:willUpdateProps",
|
|
"ReactiveChild:willUpdateProps",
|
|
"Parent:rendered",
|
|
"ReactiveChild:willRender",
|
|
"ReactiveChild:rendered",
|
|
]
|
|
`);
|
|
|
|
promise = makeDeferred();
|
|
const prom2 = promise;
|
|
child.render(); // trigger a root rendering first
|
|
parent.state.value = 2;
|
|
parent.render(true);
|
|
reactiveChild.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("0_0<div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"GrandParent:willRender",
|
|
"Parent:willUpdateProps",
|
|
"GrandParent:rendered",
|
|
"Parent:willRender",
|
|
"DelayedChild:willUpdateProps",
|
|
"ReactiveChild:willUpdateProps",
|
|
"Parent:rendered",
|
|
"ReactiveChild:willRender",
|
|
"ReactiveChild:rendered",
|
|
]
|
|
`);
|
|
|
|
prom2.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("2_1<div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"DelayedChild:willRender",
|
|
"DelayedChild:rendered",
|
|
"GrandParent:willPatch",
|
|
"Parent:willPatch",
|
|
"ReactiveChild:willPatch",
|
|
"DelayedChild:willPatch",
|
|
"DelayedChild:patched",
|
|
"ReactiveChild:patched",
|
|
"Parent:patched",
|
|
"GrandParent:patched",
|
|
]
|
|
`);
|
|
|
|
prom1.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("2_2<div></div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"DelayedChild:willRender",
|
|
"DelayedChild:rendered",
|
|
"DelayedChild:willPatch",
|
|
"DelayedChild:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("two sequential renderings before an animation frame", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.value"/>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child value="state.value"/>`;
|
|
static components = { Child };
|
|
state = useState({ value: 0 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("0");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
parent.state.value = 1;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(fixture.innerHTML).toBe("0");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
]
|
|
`);
|
|
|
|
parent.state.value = 2;
|
|
// enough microticks to wait for render + willupdateprops
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(fixture.innerHTML).toBe("0");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextTick();
|
|
// we check here that the willPatch and patched hooks are called only once
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willPatch",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("t-key on dom node having a component", async () => {
|
|
let def: any;
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.key" />`;
|
|
setup() {
|
|
onWillStart(() => def);
|
|
useLogLifecycle(this.props.key);
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
key = 1;
|
|
myComp = Child;
|
|
static template = xml`<div t-key="key"><t t-component="myComp" key="key" /></div>`;
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
|
|
def = makeDeferred();
|
|
parent.key = 2;
|
|
parent.render();
|
|
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child (1):setup",
|
|
"Child (1):willStart",
|
|
"Child (1):willRender",
|
|
"Child (1):rendered",
|
|
"Child (1):mounted",
|
|
"Child (2):setup",
|
|
"Child (2):willStart",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
parent.key = 3;
|
|
parent.render();
|
|
|
|
const prevDef = def;
|
|
def = undefined;
|
|
|
|
parent.key = 3;
|
|
parent.render();
|
|
prevDef.resolve();
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child (3):setup",
|
|
"Child (3):willStart",
|
|
"Child (3):willRender",
|
|
"Child (3):rendered",
|
|
"Child (2):willDestroy",
|
|
"Child (1):willUnmount",
|
|
"Child (1):willDestroy",
|
|
"Child (3):mounted",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("t-key on dynamic async component (toggler is never patched)", async () => {
|
|
let def: any;
|
|
class Child extends Component {
|
|
static template = xml`<div t-esc="props.key" />`;
|
|
setup() {
|
|
onWillStart(() => def);
|
|
useLogLifecycle(this.props.key);
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
key = 1;
|
|
myComp = Child;
|
|
static template = xml`<t t-component="myComp" t-key="key" key="key" />`;
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
|
|
def = makeDeferred();
|
|
parent.key = 2;
|
|
parent.render();
|
|
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child (1):setup",
|
|
"Child (1):willStart",
|
|
"Child (1):willRender",
|
|
"Child (1):rendered",
|
|
"Child (1):mounted",
|
|
"Child (2):setup",
|
|
"Child (2):willStart",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
parent.key = 3;
|
|
parent.render();
|
|
|
|
const prevDef = def;
|
|
def = undefined;
|
|
|
|
parent.key = 3;
|
|
parent.render();
|
|
prevDef.resolve();
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child (3):setup",
|
|
"Child (3):willStart",
|
|
"Child (3):willRender",
|
|
"Child (3):rendered",
|
|
"Child (2):willDestroy",
|
|
"Child (1):willUnmount",
|
|
"Child (1):willDestroy",
|
|
"Child (3):mounted",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("t-foreach with dynamic async component", async () => {
|
|
let def: any;
|
|
class Child extends Component {
|
|
static template = xml`<div t-esc="props.key" />`;
|
|
setup() {
|
|
onWillStart(() => def);
|
|
useLogLifecycle(this.props.key);
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
list: any = [[1]];
|
|
myComp = Child;
|
|
static template = xml`<t t-foreach="list" t-as="arr" t-key="arr_index">
|
|
<t t-if="arr" t-component="myComp" key="arr[0]" />
|
|
</t>`;
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
|
|
def = makeDeferred();
|
|
parent.list = [, [2]];
|
|
parent.render();
|
|
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child (1):setup",
|
|
"Child (1):willStart",
|
|
"Child (1):willRender",
|
|
"Child (1):rendered",
|
|
"Child (1):mounted",
|
|
"Child (2):setup",
|
|
"Child (2):willStart",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
parent.list = [, , [3]];
|
|
parent.render();
|
|
|
|
const prevDef = def;
|
|
def = undefined;
|
|
|
|
parent.render();
|
|
prevDef.resolve();
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Child (3):setup",
|
|
"Child (3):willStart",
|
|
"Child (3):willRender",
|
|
"Child (3):rendered",
|
|
"Child (2):willDestroy",
|
|
"Child (1):willUnmount",
|
|
"Child (1):willDestroy",
|
|
"Child (3):mounted",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("Cascading renders after microtaskTick", async () => {
|
|
const state = [{ id: 0 }, { id: 1 }];
|
|
let child: any;
|
|
let parent: any;
|
|
|
|
class Element extends Component {
|
|
static template = xml`<t t-esc="props.id" />`;
|
|
}
|
|
|
|
class Child extends Component {
|
|
static components = { Element };
|
|
static template = xml`
|
|
<t t-foreach="state" t-as="elem" t-key="elem.id">
|
|
<Element id="elem.id"/>
|
|
</t>`;
|
|
state = state;
|
|
setup() {
|
|
child = this;
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`<Child /> _ <t t-foreach="state" t-as="elem" t-key="elem.id" t-esc="elem.id"/>`;
|
|
state = state;
|
|
setup() {
|
|
parent = this;
|
|
}
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("01 _ 01");
|
|
|
|
state.push({ id: 2 });
|
|
parent.render();
|
|
child.render();
|
|
|
|
await Promise.resolve();
|
|
expect(fixture.innerHTML).toBe("01 _ 01");
|
|
state.push({ id: 3 });
|
|
parent.render();
|
|
child.render();
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("0123 _ 0123");
|
|
});
|
|
|
|
test("rendering parent twice, with different props on child and stuff", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<t t-esc="props.value"/>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<Child value="state.value"/>`;
|
|
static components = { Child };
|
|
state = useState({ value: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("1");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]
|
|
`);
|
|
|
|
parent.state.value = 2;
|
|
// wait for child to be rendered
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("1");
|
|
|
|
// trigger a render, but keep the props for child the same
|
|
parent.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("2");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"Parent:willRender",
|
|
"Child:willUpdateProps",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Parent:willPatch",
|
|
"Child:willPatch",
|
|
"Child:patched",
|
|
"Parent:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("delayed rendering, but then initial rendering is cancelled by yet another render", async () => {
|
|
const promC = makeDeferred();
|
|
let stateB: any = null;
|
|
|
|
class D extends Component {
|
|
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
increment() {
|
|
this.state.val++;
|
|
}
|
|
}
|
|
|
|
class C extends Component {
|
|
static template = xml`<D/><p><t t-esc="props.value"/></p>`;
|
|
static components = { D };
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => promC);
|
|
}
|
|
}
|
|
|
|
class B extends Component {
|
|
static template = xml`<C value="state.someValue + props.value"/>`;
|
|
static components = { C };
|
|
state = useState({ someValue: 3 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
stateB = this.state;
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`<B value="state.value"/>`;
|
|
static components = { B };
|
|
state = useState({ value: 33 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("<button>1</button><p>36</p>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"D:setup",
|
|
"D:willStart",
|
|
"C:rendered",
|
|
"D:willRender",
|
|
"D:rendered",
|
|
"D:mounted",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// update B and C, but render is blocked by C willupdateProps
|
|
stateB.someValue = 5;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
]
|
|
`);
|
|
|
|
// update D => render should be delayed, because B is currently rendering
|
|
fixture.querySelector("button")!.click();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
// update A => render should go to B and cancel it
|
|
parent.state.value = 34;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
]
|
|
`);
|
|
|
|
promC.resolve();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"D:willRender",
|
|
"D:rendered",
|
|
"D:willPatch",
|
|
"D:patched",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"C:willPatch",
|
|
"C:patched",
|
|
"B:patched",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("<button>2</button><p>39</p>");
|
|
});
|
|
|
|
test("delayed rendering, reusing fiber and stuff", async () => {
|
|
let prom1 = makeDeferred();
|
|
let prom2 = makeDeferred();
|
|
|
|
class C extends Component {
|
|
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
increment() {
|
|
this.state.val++;
|
|
}
|
|
}
|
|
|
|
class B extends Component {
|
|
static template = xml`<t t-esc="props.value"/><C />`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
let flag = false;
|
|
onWillUpdateProps(() => {
|
|
flag = true;
|
|
return prom1;
|
|
});
|
|
onRendered(async () => {
|
|
if (flag) {
|
|
await nextMicroTick();
|
|
prom2.resolve();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`<B value="state.value"/>`;
|
|
static components = { B };
|
|
state = useState({ value: 33 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("33<button>1</button>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// initiate a render in A, but is blocked in B
|
|
parent.state.value = 34;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
]
|
|
`);
|
|
|
|
// initiate a render in C => delayed because of render in A
|
|
fixture.querySelector("button")!.click();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
// wait for render in A to be completed
|
|
prom1.resolve();
|
|
await prom2;
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
]
|
|
`);
|
|
|
|
// initiate a new render in A => fiber will be reused
|
|
parent.state.value = 355;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("355<button>2</button>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"B:patched",
|
|
"A:patched",
|
|
"C:willPatch",
|
|
"C:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("delayed rendering, then component is destroyed and stuff", async () => {
|
|
let prom1 = makeDeferred();
|
|
|
|
class C extends Component {
|
|
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
increment() {
|
|
this.state.val++;
|
|
}
|
|
}
|
|
|
|
class B extends Component {
|
|
static template = xml`<t t-esc="props.value"/><t t-if="props.value lt 10"><C /></t>`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => prom1);
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`<B value="state.value"/>`;
|
|
static components = { B };
|
|
state = useState({ value: 3 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("3<button>1</button>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// initiate a render in C (so will be first task)
|
|
fixture.querySelector("button")!.click();
|
|
// initiate a render in A, but is blocked in B. the render will destroy c. also,
|
|
// it blocks the render C
|
|
parent.state.value = 34;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
]
|
|
`);
|
|
|
|
// wait for render in A to be completed
|
|
prom1.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("34");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"C:willUnmount",
|
|
"C:willDestroy",
|
|
"B:patched",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
|
|
await nextTick();
|
|
});
|
|
|
|
test("delayed rendering, reusing fiber then component is destroyed and stuff", async () => {
|
|
let prom1 = makeDeferred();
|
|
|
|
class C extends Component {
|
|
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
increment() {
|
|
this.state.val++;
|
|
}
|
|
}
|
|
|
|
class B extends Component {
|
|
static template = xml`<t t-esc="props.value"/><C />`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => prom1);
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`A<t t-if="state.value lt 15"><B value="state.value"/></t>`;
|
|
static components = { B };
|
|
state = useState({ value: 3 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("A3<button>1</button>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// initiate a render in A, but is blocked in B
|
|
parent.state.value = 5;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
]
|
|
`);
|
|
|
|
// initiate a render in C (will be delayed because of render in A)
|
|
fixture.querySelector("button")!.click();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
// initiate a render in A, that will destroy B
|
|
parent.state.value = 23;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("A");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"A:rendered",
|
|
"A:willPatch",
|
|
"B:willUnmount",
|
|
"C:willUnmount",
|
|
"C:willDestroy",
|
|
"B:willDestroy",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("another scenario with delayed rendering", async () => {
|
|
let prom1 = makeDeferred();
|
|
let onSecondRenderA = makeDeferred();
|
|
|
|
class C extends Component {
|
|
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
increment() {
|
|
this.state.val++;
|
|
}
|
|
}
|
|
|
|
class B extends Component {
|
|
static template = xml`<t t-esc="props.value"/><C />`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => prom1);
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`A<t t-if="state.value lt 15"><B value="state.value"/></t>`;
|
|
static components = { B };
|
|
state = useState({ value: 3 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
let n = 0;
|
|
onRendered(() => {
|
|
n++;
|
|
if (n === 2) {
|
|
onSecondRenderA.resolve();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("A3<button>1</button>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// initiate a render in A, but is blocked in B
|
|
parent.state.value = 5;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
]
|
|
`);
|
|
|
|
// initiate a render in C (will be delayed because of render in A)
|
|
fixture.querySelector("button")!.click();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
// initiate a render in A, that will destroy B
|
|
parent.state.value = 23;
|
|
await onSecondRenderA;
|
|
await nextMicroTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"A:rendered",
|
|
]
|
|
`);
|
|
|
|
// rerender A, but without destroying B
|
|
parent.state.value = 7;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
]
|
|
`);
|
|
|
|
prom1.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("A7<button>2</button>");
|
|
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"B:patched",
|
|
"A:patched",
|
|
"C:willPatch",
|
|
"C:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("delayed fiber does not get rendered if it was cancelled", async () => {
|
|
class D extends Component {
|
|
static template = xml`D`;
|
|
setup() {
|
|
useLogLifecycle("", true);
|
|
}
|
|
}
|
|
|
|
class C extends Component {
|
|
static template = xml`C<D/>`;
|
|
static components = { D };
|
|
setup() {
|
|
useLogLifecycle("", true);
|
|
c = this;
|
|
}
|
|
}
|
|
let c: C;
|
|
|
|
class B extends Component {
|
|
static template = xml`B<C/>`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle("", true);
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`A<B/>`;
|
|
static components = { B };
|
|
setup() {
|
|
useLogLifecycle("", true);
|
|
}
|
|
}
|
|
|
|
const a = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("ABCD");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"D:setup",
|
|
"C:rendered",
|
|
"D:willRender",
|
|
"D:rendered",
|
|
"D:mounted",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
// Start a render in C
|
|
c!.render(true);
|
|
await nextMicroTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"C:willRender",
|
|
"C:rendered",
|
|
]
|
|
`);
|
|
// Start a render in A such that C is already rendered, but D will be delayed
|
|
// (because A is rendering) then cancelled (when the render from A reaches C)
|
|
a.render(true);
|
|
// Make sure the render can go to completion (Cancelled fibers will throw when rendered)
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"D:willRender",
|
|
"D:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"C:willPatch",
|
|
"D:willPatch",
|
|
"D:patched",
|
|
"C:patched",
|
|
"B:patched",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("destroyed component causes other soon to be destroyed component to rerender, weird stuff happens", async () => {
|
|
let def = makeDeferred();
|
|
let c: any = null;
|
|
|
|
class B extends Component {
|
|
static template = xml`<t t-esc="props.value"/>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
onRendered(() => {
|
|
def.resolve();
|
|
});
|
|
onWillDestroy(() => {
|
|
c.state.val++;
|
|
c.render();
|
|
});
|
|
}
|
|
}
|
|
class C extends Component {
|
|
static template = xml`<t t-esc="state.val + props.value"/>`;
|
|
state = useState({ val: 0 });
|
|
setup() {
|
|
c = c || this;
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`
|
|
A
|
|
<t t-if="state.flag">
|
|
<B value="state.valueB"/>
|
|
<C value="state.valueC"/>
|
|
</t>`;
|
|
static components = { B, C };
|
|
state = useState({ flag: false, valueB: 1, valueC: 2 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe(" A ");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"A:rendered",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// initiate a render in A, but is blocked in B
|
|
parent.state.flag = true;
|
|
|
|
await def;
|
|
await nextMicroTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
]
|
|
`);
|
|
|
|
// initiate render in A => will cancel renders in B/C and restarts
|
|
parent.state.valueB = 2;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"B:willDestroy",
|
|
"C:willDestroy",
|
|
"A:willPatch",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
|
|
expect(fixture.innerHTML).toBe(" A 22");
|
|
});
|
|
|
|
test("delayed rendering, destruction, stuff happens", async () => {
|
|
const promC = makeDeferred();
|
|
let stateB: any = null;
|
|
|
|
class D extends Component {
|
|
static template = xml`D<button t-on-click="increment"><t t-esc="state.val"/></button>`;
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
increment() {
|
|
this.state.val++;
|
|
}
|
|
}
|
|
|
|
class C extends Component {
|
|
static template = xml`C<D/><p><t t-esc="props.value"/></p>`;
|
|
static components = { D };
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => promC);
|
|
}
|
|
}
|
|
|
|
class B extends Component {
|
|
static template = xml`B<t t-if="state.hasChild"><C value="state.someValue + props.value"/></t>`;
|
|
static components = { C };
|
|
state = useState({ someValue: 3, hasChild: true });
|
|
setup() {
|
|
useLogLifecycle();
|
|
stateB = this.state;
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`A<B value="state.value"/>`;
|
|
static components = { B };
|
|
state = useState({ value: 33 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("ABCD<button>1</button><p>36</p>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"D:setup",
|
|
"D:willStart",
|
|
"C:rendered",
|
|
"D:willRender",
|
|
"D:rendered",
|
|
"D:mounted",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// render in A, it updates B and C, but render is blocked in C
|
|
parent.state.value = 50;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:willUpdateProps",
|
|
"B:rendered",
|
|
]
|
|
`);
|
|
|
|
// update B => removes child C
|
|
stateB.hasChild = false;
|
|
// update D => render should be delayed, because AB is currently rendering
|
|
fixture.querySelector("button")!.click();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"C:willUnmount",
|
|
"D:willUnmount",
|
|
"D:willDestroy",
|
|
"C:willDestroy",
|
|
"B:patched",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("AB");
|
|
});
|
|
|
|
test("renderings, destruction, patch, stuff, ... yet another variation", async () => {
|
|
const promB = makeDeferred();
|
|
|
|
class D extends Component {
|
|
static template = xml`D<p t-on-click="increment"><t t-esc="state.val"/></p>`;
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
increment() {
|
|
this.state.val++;
|
|
}
|
|
}
|
|
|
|
// almost the same as D
|
|
class C extends Component {
|
|
static template = xml`C<span t-on-click="increment"><t t-esc="state.val"/></span>`;
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
increment() {
|
|
this.state.val++;
|
|
}
|
|
}
|
|
|
|
class B extends Component {
|
|
static template = xml`B<t t-if="props.value === 33"><C/></t>`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillUpdateProps(() => promB);
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static template = xml`A<B value="state.value"/><D/>`;
|
|
static components = { B, D };
|
|
state = useState({ value: 33 });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
const parent = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("ABC<span>1</span>D<p>1</p>");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"D:setup",
|
|
"D:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"D:willRender",
|
|
"D:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"C:mounted",
|
|
"D:mounted",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// render in A, it updates B, will remove C, stopped in B
|
|
parent.state.value = 50;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
]
|
|
`);
|
|
|
|
// update C => render should be delayed, because AB is currently rendering
|
|
fixture.querySelector("span")!.click();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
|
|
// resolve prom B => render is done, component C is destroyed
|
|
promB.resolve();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"C:willUnmount",
|
|
"C:willDestroy",
|
|
"B:patched",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("ABD<p>1</p>");
|
|
|
|
// update D => should just render completely independently
|
|
fixture.querySelector("p")!.click();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"D:willRender",
|
|
"D:rendered",
|
|
"D:willPatch",
|
|
"D:patched",
|
|
]
|
|
`);
|
|
expect(fixture.innerHTML).toBe("ABD<p>2</p>");
|
|
});
|
|
|
|
test("delayed render does not go through when t-component value changed", async () => {
|
|
class C extends Component {
|
|
static template = xml`C`;
|
|
setup() {
|
|
useLogLifecycle("", true);
|
|
}
|
|
}
|
|
|
|
class B extends Component {
|
|
static template = xml`B<t t-esc="state.val"/>`;
|
|
state = useState({ val: 1 });
|
|
setup() {
|
|
useLogLifecycle("", true);
|
|
b = this;
|
|
}
|
|
}
|
|
let b: B;
|
|
|
|
class A extends Component {
|
|
static template = xml`A<t t-component="state.component"/>`;
|
|
state: { component: ComponentConstructor } = useState({ component: B });
|
|
setup() {
|
|
useLogLifecycle("", true);
|
|
}
|
|
}
|
|
|
|
const a = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("AB1");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
// start a render in B
|
|
b!.state.val = 2;
|
|
// start a render in A, invalidating the scheduled render of B, which could crash if executed.
|
|
a.state.component = C;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("AC");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"C:setup",
|
|
"A:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"A:willPatch",
|
|
"B:willUnmount",
|
|
"B:willDestroy",
|
|
"C:mounted",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("delayed render is not cancelled by upcoming render", async () => {
|
|
let b: any;
|
|
|
|
class B extends Component {
|
|
static template = xml`
|
|
<t t-esc="props.state.groups.length"/>
|
|
<t t-esc="props.state.config.test"/>`;
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
b = this;
|
|
}
|
|
}
|
|
|
|
class A extends Component {
|
|
static components = { B };
|
|
static template = xml`<B state="state" isEmpty="state.groups.length === 0"/>`;
|
|
|
|
state = useState({ groups: [], config: { test: "initial" } });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
await mount(A, fixture);
|
|
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
expect(fixture.innerHTML).toBe("0initial");
|
|
|
|
b.props.state.config.test = "red";
|
|
b.props.state.groups.push(1);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
b.props.state.config.test = "black";
|
|
b.props.state.groups.push(1);
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
]
|
|
`);
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("2black");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:willUpdateProps",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"A:willPatch",
|
|
"B:willPatch",
|
|
"B:patched",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("components are not destroyed between animation frame", async () => {
|
|
const def = makeDeferred();
|
|
class C extends Component {
|
|
static template = xml`C`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
class B extends Component {
|
|
static template = xml`B<C/>`;
|
|
static components = { C };
|
|
setup() {
|
|
useLogLifecycle();
|
|
onWillStart(() => {
|
|
return def;
|
|
});
|
|
}
|
|
}
|
|
class A extends Component {
|
|
static template = xml`A<B t-if="state.flag"/>`;
|
|
static components = { B };
|
|
|
|
state = useState({ flag: false });
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const a = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("A");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"A:rendered",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
// turn the flag on, this will render A and stops at B because of def
|
|
a.state.flag = true;
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
]
|
|
`);
|
|
|
|
// force a render of A
|
|
// => owl will need to create a new B component
|
|
// => initial B component will be cancelled
|
|
a.render();
|
|
await nextMicroTick();
|
|
expect([
|
|
// note that B is not destroyed here. It is cancelled instead
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
]).toBeLogged();
|
|
|
|
// resolve def, so B render is unblocked
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"B:willRender",
|
|
"C:setup",
|
|
"C:willStart",
|
|
"B:rendered",
|
|
"C:willRender",
|
|
"C:rendered",
|
|
"B:willDestroy",
|
|
"A:willPatch",
|
|
"C:mounted",
|
|
"B:mounted",
|
|
"A:patched",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("component destroyed just after render", async () => {
|
|
let stateB: any;
|
|
|
|
class B extends Component {
|
|
static template = xml`B<t t-esc="state.value"/>`;
|
|
state = useState({ value: 1 });
|
|
setup() {
|
|
stateB = this.state;
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
class A extends Component {
|
|
static template = xml`<B/>`;
|
|
static components = { B };
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
const a = await mount(A, fixture);
|
|
expect(fixture.innerHTML).toBe("B1");
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:setup",
|
|
"A:willStart",
|
|
"A:willRender",
|
|
"B:setup",
|
|
"B:willStart",
|
|
"A:rendered",
|
|
"B:willRender",
|
|
"B:rendered",
|
|
"B:mounted",
|
|
"A:mounted",
|
|
]
|
|
`);
|
|
|
|
stateB!.value++; // force a render of B
|
|
await nextMicroTick(); // wait for B render to actually start
|
|
a.__owl__.app.destroy();
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
|
Array [
|
|
"A:willUnmount",
|
|
"B:willUnmount",
|
|
"B:willDestroy",
|
|
"A:willDestroy",
|
|
]
|
|
`);
|
|
await nextTick();
|
|
// check that B was not rendered after being destroyed
|
|
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
|
|
});
|
|
|
|
// test.skip("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.skip("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>"
|
|
// );
|
|
// });
|
|
// });
|