import {
App,
Component,
mount,
onMounted,
onRendered,
onWillStart,
onWillUnmount,
onWillUpdateProps,
useState,
xml,
} from "../../src";
import { Fiber } from "../../src/component/fibers";
import { Scheduler } from "../../src/component/scheduler";
import { status } from "../../src/component/status";
import {
makeDeferred,
makeTestFixture,
nextMicroTick,
nextTick,
snapshotEverything,
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`
`;
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(["W:setup", "W:willStart", "W:willDestroy"]).toBeLogged();
});
});
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`child: `;
setup() {
useLogLifecycle();
n++;
onWillStart(() => def);
}
}
class W extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
}
const w = await mount(W, fixture);
expect(["W:setup", "W:willStart", "W:willRender", "W:rendered", "W:mounted"]).toBeLogged();
expect(n).toBe(0);
w.state.val = 2;
await nextMicroTick();
await nextMicroTick();
expect(n).toBe(1);
expect(["W:willRender", "Child:setup", "Child:willStart", "W:rendered"]).toBeLogged();
w.state.val = 3;
await nextMicroTick();
await nextMicroTick();
expect(n).toBe(2);
expect([
"W:willRender",
"Child:willDestroy",
"Child:setup",
"Child:willStart",
"W:rendered",
]).toBeLogged();
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("child:3
");
expect(Object.values(w.__owl__.children).length).toBe(1);
expect([
"Child:willRender",
"Child:rendered",
"W:willPatch",
"Child:mounted",
"W:patched",
]).toBeLogged();
});
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 `;
static components = { Child };
state = useState({ hasChild: false });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
expect(fixture.innerHTML).toBe("parent");
parent.state.hasChild = true;
await nextTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
]).toBeLogged();
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` `;
setup() {
useLogLifecycle();
onWillStart(() => defA);
}
getValue() {
nbRenderings++;
return "a";
}
}
class ChildB extends Component {
static template = xml`b `;
setup() {
useLogLifecycle();
onWillStart(() => defB);
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA, ChildB };
state = useState({ flagA: false, flagB: false });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
expect(fixture.innerHTML).toBe("");
parent.state.flagA = true;
await nextTick();
expect(["Parent:willRender", "ChildA:setup", "ChildA:willStart", "Parent:rendered"]).toBeLogged();
expect(fixture.innerHTML).toBe("");
parent.state.flagB = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"Parent:willRender",
"ChildA:willDestroy",
"ChildA:setup",
"ChildA:willStart",
"ChildB:setup",
"ChildB:willStart",
"Parent:rendered",
]).toBeLogged();
defB.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(nbRenderings).toBe(0);
expect(["ChildB:willRender", "ChildB:rendered"]).toBeLogged();
defA.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("a b ");
expect(nbRenderings).toBe(1);
expect([
"ChildA:willRender",
"ChildA:rendered",
"Parent:willPatch",
"ChildB:mounted",
"ChildA:mounted",
"Parent:patched",
]).toBeLogged();
});
test("creating two async components, scenario 2", async () => {
let defA = makeDeferred();
let defB = makeDeferred();
class ChildA extends Component {
static template = xml`a `;
setup() {
useLogLifecycle();
onWillUpdateProps(() => defA);
}
}
class ChildB extends Component {
static template = xml`b `;
setup() {
useLogLifecycle();
onWillStart(() => defB);
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA, ChildB };
state = useState({ valA: 1, valB: 2, flagB: false });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
]).toBeLogged();
expect(fixture.innerHTML).toBe("a1
");
parent.state.valA = 2;
await nextTick();
expect(["Parent:willRender", "ChildA:willUpdateProps", "Parent:rendered"]).toBeLogged();
expect(fixture.innerHTML).toBe("a1
");
parent.state.flagB = true;
await nextTick();
expect(fixture.innerHTML).toBe("a1
");
expect([
"Parent:willRender",
"ChildA:willUpdateProps",
"ChildB:setup",
"ChildB:willStart",
"Parent:rendered",
]).toBeLogged();
defB.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("a1
");
expect(["ChildB:willRender", "ChildB:rendered"]).toBeLogged();
defA.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("a2 b2
");
expect([
"ChildA:willRender",
"ChildA:rendered",
"Parent:willPatch",
"ChildA:willPatch",
"ChildB:mounted",
"ChildA:patched",
"Parent:patched",
]).toBeLogged();
});
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`a `;
setup() {
useLogLifecycle();
onWillUpdateProps(() => defA);
}
}
class ChildB extends Component {
static template = xml`b `;
setup() {
useLogLifecycle();
onWillStart(() => defB);
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA, ChildB };
state = useState({ valA: 1, valB: 2, flagB: false });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
]).toBeLogged();
expect(fixture.innerHTML).toBe("a1
");
parent.state.valA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("a1
");
expect(["Parent:willRender", "ChildA:willUpdateProps", "Parent:rendered"]).toBeLogged();
parent.state.flagB = true;
await nextTick();
expect(fixture.innerHTML).toBe("a1
");
expect([
"Parent:willRender",
"ChildA:willUpdateProps",
"ChildB:setup",
"ChildB:willStart",
"Parent:rendered",
]).toBeLogged();
defB.resolve();
expect(fixture.innerHTML).toBe("a1
");
expect([]).toBeLogged();
defA.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("a2 b2
");
expect([
"ChildB:willRender",
"ChildB:rendered",
"ChildA:willRender",
"ChildA:rendered",
"Parent:willPatch",
"ChildA:willPatch",
"ChildB:mounted",
"ChildA:patched",
"Parent:patched",
]).toBeLogged();
});
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` `;
setup() {
onWillUpdateProps(() => defs[index++]);
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA };
state = useState({ valA: 1 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1
");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.valA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("1
");
expect(["Parent:willRender", "ChildA:willUpdateProps", "Parent:rendered"]).toBeLogged();
parent.state.valA = 3;
await nextTick();
expect(fixture.innerHTML).toBe("1
");
expect(["Parent:willRender", "ChildA:willUpdateProps", "Parent:rendered"]).toBeLogged();
defs[0].resolve();
await Promise.resolve();
defs[1].resolve();
await nextTick();
expect(fixture.innerHTML).toBe("3
");
expect([
"ChildA:willRender",
"ChildA:rendered",
"Parent:willPatch",
"ChildA:willPatch",
"ChildA:patched",
"Parent:patched",
]).toBeLogged();
});
test("update a sub-component twice in the same frame, 2", async () => {
class ChildA extends Component {
static template = xml` `;
setup() {
useLogLifecycle();
}
val() {
return this.props.val;
}
}
class Parent extends Component {
static template = xml`
`;
static components = { ChildA };
state = useState({ valA: 1 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
]).toBeLogged();
expect(fixture.innerHTML).toBe("1
");
parent.state.valA = 2;
await nextMicroTick();
await nextMicroTick();
expect(["Parent:willRender", "ChildA:willUpdateProps", "Parent:rendered"]).toBeLogged();
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(["ChildA:willRender", "ChildA:rendered"]).toBeLogged();
expect(fixture.innerHTML).toBe("1
");
parent.state.valA = 3;
await nextMicroTick();
await nextMicroTick();
expect(["Parent:willRender", "ChildA:willUpdateProps", "Parent:rendered"]).toBeLogged();
await nextMicroTick();
// same as above
await nextMicroTick();
expect(["ChildA:willRender", "ChildA:rendered"]).toBeLogged();
expect(fixture.innerHTML).toBe("1
");
await nextTick();
expect(fixture.innerHTML).toBe("3
");
expect(["Parent:willPatch", "ChildA:willPatch", "ChildA:patched", "Parent:patched"]).toBeLogged();
});
test("properly behave when destroyed/unmounted while rendering ", async () => {
const def = makeDeferred();
class SubChild extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
onWillUpdateProps(() => {
return def;
});
}
}
class Child extends Component {
static template = xml`
`;
static components = { SubChild };
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: true, val: "Framboise Lindemans" });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"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",
]).toBeLogged();
// 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("");
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"SubChild:willUpdateProps",
"Child:rendered",
]).toBeLogged();
// 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("
");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"SubChild:willUnmount",
"SubChild:willDestroy",
"Child:willDestroy",
"Parent:patched",
]).toBeLogged();
// we now resolve def, so the child rendering is now complete.
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("
");
expect([]).toBeLogged();
});
test("rendering component again in next microtick", async () => {
class Child extends Component {
static template = xml`Child
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
Click
`;
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("Click
");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Parent:willRender",
"Child:willDestroy",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
]).toBeLogged();
});
test("concurrent renderings scenario 1", async () => {
const def = makeDeferred();
let stateB: any = null;
class ComponentC extends Component {
static template = xml` `;
setup() {
useLogLifecycle();
onWillUpdateProps(() => def);
}
someValue() {
return this.props.fromB;
}
}
ComponentC.prototype.someValue = jest.fn(ComponentC.prototype.someValue);
class ComponentB extends Component {
static template = xml`
`;
static components = { ComponentC };
state = useState({ fromB: "b" });
setup() {
stateB = this.state;
useLogLifecycle();
}
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB };
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"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",
]).toBeLogged();
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:rendered",
]).toBeLogged();
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:rendered",
]).toBeLogged();
expect(ComponentC.prototype.someValue).toBeCalledTimes(1);
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentC.prototype.someValue).toBeCalledTimes(2);
expect([
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
"ComponentC:patched",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
});
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` `;
setup() {
useLogLifecycle();
onWillUpdateProps(() => defs[index++]);
}
}
class ComponentB extends Component {
static template = xml`
`;
static components = { ComponentC };
state = useState({ fromB: "b" });
setup() {
useLogLifecycle();
stateB = this.state;
}
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB };
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"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",
]).toBeLogged();
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:rendered",
]).toBeLogged();
debugger;
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:rendered",
]).toBeLogged();
defs[1].resolve(); // resolve rendering initiated in B
await nextTick();
debugger
expect([
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
"ComponentC:patched",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("");
defs[0].resolve(); // resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([]).toBeLogged();
});
test("concurrent renderings scenario 2bis", async () => {
const defs = [makeDeferred(), makeDeferred()];
let index = 0;
let stateB: any = null;
class ComponentC extends Component {
static template = xml` `;
setup() {
useLogLifecycle();
onWillUpdateProps(() => defs[index++]);
}
}
class ComponentB extends Component {
static template = xml`
`;
static components = { ComponentC };
state = useState({ fromB: "b" });
setup() {
useLogLifecycle();
stateB = this.state;
}
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB };
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"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",
]).toBeLogged();
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:rendered",
]).toBeLogged();
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:rendered",
]).toBeLogged();
defs[0].resolve(); // resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe(""); // TODO: is this what we want?? 2b could be ok too
expect([]).toBeLogged();
defs[1].resolve(); // resolve rendering initiated in B
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
"ComponentC:patched",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
});
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` `;
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` `;
static components = { ComponentD };
state = useState({ fromC: "c" });
setup() {
useLogLifecycle();
stateC = this.state;
}
}
class ComponentB extends Component {
static template = xml`
`;
static components = { ComponentC };
setup() {
useLogLifecycle();
onWillUpdateProps(() => defB);
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"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",
]).toBeLogged();
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
stateC.fromC = "d";
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([]).toBeLogged();
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentC:rendered",
]).toBeLogged();
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentD:willRender",
"ComponentD:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
"ComponentD:willPatch",
"ComponentD:patched",
"ComponentC:patched",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
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` `;
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` `;
static components = { ComponentD };
state = useState({ fromC: "c" });
setup() {
useLogLifecycle();
stateC = this.state;
}
}
class ComponentB extends Component {
static template = xml`
`;
static components = { ComponentC };
setup() {
useLogLifecycle();
onWillUpdateProps(() => defB);
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"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",
]).toBeLogged();
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
stateC.fromC = "d";
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([]).toBeLogged();
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentC:rendered",
]).toBeLogged();
defsD[1].resolve(); // completely resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentD.prototype.someValue).toBeCalledTimes(1);
expect([]).toBeLogged();
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
expect([
"ComponentD:willRender",
"ComponentD:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
"ComponentD:willPatch",
"ComponentD:patched",
"ComponentC:patched",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
});
test("concurrent renderings scenario 5", async () => {
const defsB = [makeDeferred(), makeDeferred()];
let index = 0;
class ComponentB extends Component {
static template = xml`
`;
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`
`;
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
]).toBeLogged();
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
component.state.fromA = 3;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
defsB[0].resolve(); // resolve first re-rendering (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(1);
expect([]).toBeLogged();
defsB[1].resolve(); // resolve second re-rendering
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
expect([
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
});
test("concurrent renderings scenario 6", async () => {
const defsB = [makeDeferred(), makeDeferred()];
let index = 0;
class ComponentB extends Component {
static template = xml`
`;
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`
`;
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
]).toBeLogged();
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
component.state.fromA = 3;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
defsB[1].resolve(); // resolve second re-rendering
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
expect([
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
defsB[0].resolve(); // resolve first re-rendering (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
expect([]).toBeLogged();
});
test("concurrent renderings scenario 7", async () => {
class ComponentB extends Component {
static template = xml`
`;
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`
`;
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(1);
expect([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
]).toBeLogged();
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(ComponentB.prototype.someValue).toBeCalledTimes(2);
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
});
test("concurrent renderings scenario 8", async () => {
const def = makeDeferred();
let stateB: any = null;
class ComponentB extends Component {
static template = xml`
`;
state = useState({ fromB: "b" });
setup() {
useLogLifecycle();
stateB = this.state;
onWillUpdateProps(() => def);
}
}
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`
`;
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
]).toBeLogged();
component.state.fromA = 2;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([]).toBeLogged();
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
});
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` `;
setup() {
useLogLifecycle();
}
}
class ComponentC extends Component {
static template = xml`
`;
static components = { ComponentD };
state = useState({ fromC: "b1" });
setup() {
stateC = this.state;
useLogLifecycle();
}
}
class ComponentB extends Component {
static template = xml` `;
setup() {
onWillUpdateProps(() => def);
useLogLifecycle();
}
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB, ComponentC };
state = useState({ fromA: "a1" });
setup() {
useLogLifecycle();
}
}
const component = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"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",
]).toBeLogged();
component.state.fromA = "a2";
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentC:willUpdateProps",
"ComponentA:rendered",
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentC:rendered",
"ComponentD:willRender",
"ComponentD:rendered",
]).toBeLogged();
stateC.fromC = "b2";
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentC:rendered",
"ComponentD:willRender",
"ComponentD:rendered",
]).toBeLogged();
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentC:willPatch",
"ComponentD:willPatch",
"ComponentB:willPatch",
"ComponentB:patched",
"ComponentD:patched",
"ComponentC:patched",
"ComponentA:patched",
]).toBeLogged();
});
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` `;
setup() {
useLogLifecycle();
onWillStart(() => defC);
}
get value() {
rendered++;
return this.props.value;
}
}
class ComponentB extends Component {
static template = xml`
`;
state = useState({ hasChild: false });
static components = { ComponentC };
setup() {
useLogLifecycle();
stateB = this.state;
onWillUpdateProps(() => defB);
}
}
class ComponentA extends Component {
static template = xml`
`;
static components = { ComponentB };
state = useState({ value: 1 });
setup() {
useLogLifecycle();
}
}
const componentA = await mount(ComponentA, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
]).toBeLogged();
stateB.hasChild = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentB:willRender",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentB:rendered",
]).toBeLogged();
componentA.state.value = 2;
defC.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
defB.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(rendered).toBe(1);
expect([
"ComponentB:willRender",
"ComponentC:willDestroy",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:mounted",
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
});
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` | `;
val = 3;
setup() {
useLogLifecycle();
onWillUpdateProps(() => {
child = this;
return def;
});
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ valA: 1 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1|3
");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.valA = 2;
await nextTick();
setTimeout(() => {
def.resolve();
}, 20);
child.val = 5;
child.render();
await def;
await nextTick();
expect(fixture.innerHTML).toBe("2|5
");
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged();
});
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` `;
setup() {
useLogLifecycle();
onWillUpdateProps(() => def);
}
}
let rendered = 0;
class Parent extends Component {
static template = xml`
`;
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("1
");
expect(rendered).toBe(1);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.val = 2;
await nextTick();
expect(fixture.innerHTML).toBe("1
");
expect(rendered).toBe(2);
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
parent.state.val = 3;
parent.state.val = 4;
await nextTick();
expect(fixture.innerHTML).toBe("1
");
expect(rendered).toBe(3);
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("4
");
expect(rendered).toBe(3);
expect([
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged();
});
test("concurrent renderings scenario 13", async () => {
let lastChild: any = null;
class Child extends Component {
static template = xml` `;
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`
`;
static components = { Child };
state = useState({ bool: false });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("0
");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("1
");
expect(["Child:willPatch", "Child:patched"]).toBeLogged();
parent.state.bool = true;
await nextTick(); // wait for this change to be applied
expect([
"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",
]).toBeLogged();
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("0 1
");
expect(["Child:willPatch", "Child:patched", "Child:willPatch", "Child:patched"]).toBeLogged();
});
test("concurrent renderings scenario 14", async () => {
let b: B | undefined = undefined;
let c: C | undefined = undefined;
class C extends Component {
static template = xml`
`;
state = useState({ fromC: 3 });
setup() {
useLogLifecycle();
c = this;
}
}
class B extends Component {
static template = xml`
`;
static components = { C };
setup() {
useLogLifecycle();
b = this;
}
state = useState({ fromB: 2 });
}
class A extends Component {
static template = xml`
`;
static components = { B };
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const a = await mount(A, fixture);
expect(fixture.innerHTML).toBe("
1 2 3
");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// trigger a 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("
1 2 3
");
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
]).toBeLogged();
// 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(
"
11 12 13
"
);
expect([
"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",
]).toBeLogged();
});
test("concurrent renderings scenario 15", async () => {
let b: B | undefined = undefined;
let c: C | undefined = undefined;
class C extends Component {
static template = xml`
`;
state = useState({ fromC: 3 });
setup() {
useLogLifecycle();
c = this;
}
}
class B extends Component {
static template = xml`
`;
static components = { C };
setup() {
useLogLifecycle();
b = this;
}
state = useState({ fromB: 2 });
}
class A extends Component {
static template = xml`
`;
static components = { B };
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const app = new App(A);
const a = await app.mount(fixture);
expect(fixture.innerHTML).toBe("
1 2 3
");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// trigger a 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("
1 2 3
");
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
]).toBeLogged();
// 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("
1 2 3
");
expect([]).toBeLogged();
// wait a bit and simulate another flush (we expect nothing to change as well)
await nextMicroTick();
app.scheduler.flush();
expect(fixture.innerHTML).toBe("
1 2 3
");
expect(["C:willRender", "C:rendered"]).toBeLogged();
await nextTick();
expect(fixture.innerHTML).toBe(
"
11 12 13
"
);
expect([
"B:willRender",
"C:willUpdateProps",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"B:willPatch",
"C:willPatch",
"C:patched",
"B:patched",
"A:patched",
]).toBeLogged();
});
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`
: : :
`;
static components = { D };
state = { fromC: 3 }; // not reactive
setup() {
useLogLifecycle();
c = this;
}
}
class B extends Component {
static template = xml` `;
static components = { C };
setup() {
useLogLifecycle();
b = this;
}
state = { fromB: 2 };
}
class A extends Component {
static template = xml` `;
static components = { B };
state = useState({ fromA: 1 });
setup() {
useLogLifecycle();
}
}
const a = await mount(A, fixture);
expect(fixture.innerHTML).toBe("1:2:3: ");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// trigger a 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([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
"C:willRender",
"C:rendered",
]).toBeLogged();
// 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([
"C:willRender",
"D:setup",
"D:willStart",
"C:rendered",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
"C:willRender",
"D:willDestroy",
"D:setup",
"D:willStart",
"C:rendered",
]).toBeLogged();
// 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([
"D:willRender",
"D:rendered",
"A:willPatch",
"B:willPatch",
"C:willPatch",
"D:mounted",
"C:patched",
"B:patched",
"A:patched",
]).toBeLogged();
});
test("calling render in destroy", async () => {
let a: any = null;
let c: any = null;
class C extends Component {
static template = xml`
`;
}
let flag = false;
class B extends Component {
static template = xml` `;
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` `;
static components = { B };
state = "a";
key = 1;
setup() {
a = this;
}
}
const app = new App(A);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("a
");
expect(["B:setup", "B:willStart", "B:willRender", "B:rendered", "B:mounted"]).toBeLogged();
a.state = "A";
a.key = 2;
a.render();
await nextTick();
// this nextTick is critical, otherwise jest may silently swallow errors
await nextTick();
expect([
"B:setup",
"B:willStart",
"B:willRender",
"B:rendered",
"B:willUnmount",
"B:willDestroy",
"B:mounted",
"B:willRender",
"B:rendered",
"B:willPatch",
"B:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("A
");
});
test("change state and call manually render: no unnecessary rendering", async () => {
let numberOfRender = 0;
class Test extends Component {
static template = xml`
`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
get value() {
numberOfRender++;
return this.state.val;
}
}
const test = await mount(Test, fixture);
expect([
"Test:setup",
"Test:willStart",
"Test:willRender",
"Test:rendered",
"Test:mounted",
]).toBeLogged();
expect(fixture.innerHTML).toBe("1
");
expect(numberOfRender).toBe(1);
test.state.val = 2;
test.render();
await nextTick();
expect(fixture.innerHTML).toBe("2
");
expect(numberOfRender).toBe(2);
expect(["Test:willRender", "Test:rendered", "Test:willPatch", "Test:patched"]).toBeLogged();
});
test("changing state before first render does not trigger a render", async () => {
let renders = 0;
class TestW extends Component {
static template = xml`
`;
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([
"TestW:setup",
"TestW:willStart",
"TestW:willRender",
"TestW:rendered",
"TestW:mounted",
]).toBeLogged();
await nextTick();
expect(renders).toBe(1);
expect(fixture.innerHTML).toBe("3
");
expect([]).toBeLogged();
});
test("changing state before first render does not trigger a render (with parent)", async () => {
let renders = 0;
class TestW extends Component {
static template = xml`
`;
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`
`;
setup() {
useLogLifecycle();
}
state = useState({ flag: false });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("
");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(renders).toBe(1);
expect([
"Parent:willRender",
"TestW:setup",
"TestW:willStart",
"Parent:rendered",
"TestW:willRender",
"TestW:rendered",
"Parent:willPatch",
"TestW:mounted",
"Parent:patched",
]).toBeLogged();
});
test("two renderings initiated between willPatch and patched", async () => {
let parent: any = null;
class Panel extends Component {
static template = xml` `;
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``;
static components = { Panel };
state = useState({ panel: "Panel1", flag: true });
setup() {
useLogLifecycle();
parent = this;
}
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
expect([
"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",
]).toBeLogged();
await nextMicroTick();
expect(["Panel:willRender", "Panel:rendered"]).toBeLogged();
await nextTick();
expect(["Parent:willPatch", "Panel:willPatch", "Panel:patched", "Parent:patched"]).toBeLogged();
expect(fixture.innerHTML).toBe("");
parent.state.panel = "Panel2";
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"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",
]).toBeLogged();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"Panel:willRender",
"Panel:rendered",
"Parent:willPatch",
"Panel:willPatch",
"Panel:patched",
"Parent:patched",
]).toBeLogged();
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("
");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Panel:willUnmount",
"Panel:willDestroy",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
]).toBeLogged();
await nextTick();
expect(fixture.innerHTML).toBe("
");
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
});
test("parent and child rendered at exact same time", async () => {
let child: any = null;
class Child extends Component {
static template = xml` `;
setup() {
child = this;
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = { value: 0 };
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("0");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.value = 1;
parent.render();
child.render();
await nextTick();
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged();
});
test("delay willUpdateProps", async () => {
let promise: any = null;
let child: any;
class Child extends Component {
static template = xml` _ `;
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` `;
static components = { Child };
state = { value: 0 };
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("0_0");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
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(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
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([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged();
prom1.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("2_2");
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
});
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` _ `;
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`
`;
setup() {
reactiveChild = this;
useLogLifecycle();
}
}
// The parent of everybody, we also manually trigger render on it.
class Parent extends Component {
static template = xml` `;
static components = { DelayedChild, ReactiveChild };
setup() {
useLogLifecycle();
}
}
class GrandParent extends Component {
static template = xml` `;
static components = { Parent };
state = { value: 0 };
setup() {
useLogLifecycle();
}
}
const parent = await mount(GrandParent, fixture);
expect(fixture.innerHTML).toBe("0_0
");
expect([
"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",
]).toBeLogged();
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
");
expect([
"GrandParent:willRender",
"Parent:willUpdateProps",
"GrandParent:rendered",
"Parent:willRender",
"DelayedChild:willUpdateProps",
"ReactiveChild:willUpdateProps",
"Parent:rendered",
"ReactiveChild:willRender",
"ReactiveChild:rendered",
]).toBeLogged();
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
");
expect([
"GrandParent:willRender",
"Parent:willUpdateProps",
"GrandParent:rendered",
"Parent:willRender",
"DelayedChild:willUpdateProps",
"ReactiveChild:willUpdateProps",
"Parent:rendered",
"ReactiveChild:willRender",
"ReactiveChild:rendered",
]).toBeLogged();
prom2.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("2_1
");
expect([
"DelayedChild:willRender",
"DelayedChild:rendered",
"GrandParent:willPatch",
"Parent:willPatch",
"ReactiveChild:willPatch",
"DelayedChild:willPatch",
"DelayedChild:patched",
"ReactiveChild:patched",
"Parent:patched",
"GrandParent:patched",
]).toBeLogged();
prom1.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("2_2
");
expect([
"DelayedChild:willRender",
"DelayedChild:rendered",
"DelayedChild:willPatch",
"DelayedChild:patched",
]).toBeLogged();
});
test("two sequential renderings before an animation frame", async () => {
class Child extends Component {
static template = xml` `;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ value: 0 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("0");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.value = 1;
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect(fixture.innerHTML).toBe("0");
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
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([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
await nextTick();
// we check here that the willPatch and patched hooks are called only once
expect(["Parent:willPatch", "Child:willPatch", "Child:patched", "Parent:patched"]).toBeLogged();
});
test("t-key on dom node having a component", async () => {
let def: any;
class Child extends Component {
static template = xml` `;
setup() {
onWillStart(() => def);
useLogLifecycle(this.props.key);
}
}
class Parent extends Component {
key = 1;
myComp = Child;
static template = xml`
`;
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1
");
def = makeDeferred();
parent.key = 2;
parent.render();
await nextTick();
expect([
"Child (1):setup",
"Child (1):willStart",
"Child (1):willRender",
"Child (1):rendered",
"Child (1):mounted",
"Child (2):setup",
"Child (2):willStart",
]).toBeLogged();
expect(fixture.innerHTML).toBe("1
");
parent.key = 3;
parent.render();
const prevDef = def;
def = undefined;
parent.key = 3;
parent.render();
prevDef.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("3
");
expect([
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted",
]).toBeLogged();
});
test("t-key on dynamic async component (toggler is never patched)", async () => {
let def: any;
class Child extends Component {
static template = xml`
`;
setup() {
onWillStart(() => def);
useLogLifecycle(this.props.key);
}
}
class Parent extends Component {
key = 1;
myComp = Child;
static template = xml` `;
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1
");
def = makeDeferred();
parent.key = 2;
parent.render();
await nextTick();
expect([
"Child (1):setup",
"Child (1):willStart",
"Child (1):willRender",
"Child (1):rendered",
"Child (1):mounted",
"Child (2):setup",
"Child (2):willStart",
]).toBeLogged();
expect(fixture.innerHTML).toBe("1
");
parent.key = 3;
parent.render();
const prevDef = def;
def = undefined;
parent.key = 3;
parent.render();
prevDef.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("3
");
expect([
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted",
]).toBeLogged();
});
test("t-foreach with dynamic async component", async () => {
let def: any;
class Child extends Component {
static template = xml`
`;
setup() {
onWillStart(() => def);
useLogLifecycle(this.props.key);
}
}
class Parent extends Component {
list: any = [[1]];
myComp = Child;
static template = xml`
`;
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1
");
def = makeDeferred();
parent.list = [, [2]];
parent.render();
await nextTick();
expect([
"Child (1):setup",
"Child (1):willStart",
"Child (1):willRender",
"Child (1):rendered",
"Child (1):mounted",
"Child (2):setup",
"Child (2):willStart",
]).toBeLogged();
expect(fixture.innerHTML).toBe("1
");
parent.list = [, , [3]];
parent.render();
const prevDef = def;
def = undefined;
parent.render();
prevDef.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("3
");
expect([
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted",
]).toBeLogged();
});
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` `;
}
class Child extends Component {
static components = { Element };
static template = xml`
`;
state = state;
setup() {
child = this;
}
}
class Parent extends Component {
static components = { Child };
static template = xml` _ `;
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` `;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ value: 1 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.value = 2;
// wait for child to be rendered
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
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([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged();
});
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` `;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class C extends Component {
static template = xml`
`;
static components = { D };
setup() {
useLogLifecycle();
onWillUpdateProps(() => promC);
}
}
class B extends Component {
static template = xml` `;
static components = { C };
state = useState({ someValue: 3 });
setup() {
useLogLifecycle();
stateB = this.state;
}
}
class A extends Component {
static template = xml` `;
static components = { B };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("1 36
");
expect([
"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",
]).toBeLogged();
// update B and C, but render is blocked by C willupdateProps
stateB.someValue = 5;
await nextTick();
expect(["B:willRender", "C:willUpdateProps", "B:rendered"]).toBeLogged();
// update D => render should be delayed, because B is currently rendering
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
// update A => render should go to B and cancel it
parent.state.value = 34;
await nextTick();
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
]).toBeLogged();
promC.resolve();
await nextTick();
expect([
"C:willRender",
"C:rendered",
"D:willRender",
"D:rendered",
"D:willPatch",
"D:patched",
"A:willPatch",
"B:willPatch",
"C:willPatch",
"C:patched",
"B:patched",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("2 39
");
});
test("delayed rendering, reusing fiber and stuff", async () => {
let prom1 = makeDeferred();
let prom2 = makeDeferred();
class C extends Component {
static template = xml` `;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml` `;
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` `;
static components = { B };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("331 ");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.value = 34;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// initiate a render in C => delayed because of render in A
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
// wait for render in A to be completed
prom1.resolve();
await prom2;
expect(["B:willRender", "B:rendered", "C:willRender", "C:rendered"]).toBeLogged();
// initiate a new render in A => fiber will be reused
parent.state.value = 355;
await nextTick();
expect(fixture.innerHTML).toBe("3552 ");
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"B:patched",
"A:patched",
"C:willPatch",
"C:patched",
]).toBeLogged();
});
test("delayed rendering, then component is destroyed and stuff", async () => {
let prom1 = makeDeferred();
class C extends Component {
static template = xml` `;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml` `;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => prom1);
}
}
class A extends Component {
static template = xml` `;
static components = { B };
state = useState({ value: 3 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("31 ");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// 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(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// wait for render in A to be completed
prom1.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("34");
expect([
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"C:willUnmount",
"C:willDestroy",
"B:patched",
"A:patched",
]).toBeLogged();
await nextTick();
});
test("delayed rendering, reusing fiber then component is destroyed and stuff", async () => {
let prom1 = makeDeferred();
class C extends Component {
static template = xml` `;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml` `;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => prom1);
}
}
class A extends Component {
static template = xml`A `;
static components = { B };
state = useState({ value: 3 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("A31 ");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.value = 5;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// initiate a render in C (will be delayed because of render in A)
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
// initiate a render in A, that will destroy B
parent.state.value = 23;
await nextTick();
expect(fixture.innerHTML).toBe("A");
expect([
"A:willRender",
"A:rendered",
"A:willPatch",
"B:willUnmount",
"C:willUnmount",
"C:willDestroy",
"B:willDestroy",
"A:patched",
]).toBeLogged();
});
test("another scenario with delayed rendering", async () => {
let prom1 = makeDeferred();
let onSecondRenderA = makeDeferred();
class C extends Component {
static template = xml` `;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml` `;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => prom1);
}
}
class A extends Component {
static template = xml`A `;
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("A31 ");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.value = 5;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// initiate a render in C (will be delayed because of render in A)
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
// initiate a render in A, that will destroy B
parent.state.value = 23;
await onSecondRenderA;
await nextMicroTick();
expect(["A:willRender", "A:rendered"]).toBeLogged();
// rerender A, but without destroying B
parent.state.value = 7;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
prom1.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("A72 ");
expect([
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"B:willPatch",
"B:patched",
"A:patched",
"C:willPatch",
"C:patched",
]).toBeLogged();
});
// test.skip("components with shouldUpdate=false", async () => {
// const state = { p: 1, cc: 10 };
// class ChildChild extends Component {
// static template = xml`
//
// child child:
//
`;
// state = state;
// shouldUpdate() {
// return false;
// }
// }
// class Child extends Component {
// static components = { ChildChild };
// static template = xml`
//
// child
//
//
`;
// shouldUpdate() {
// return false;
// }
// }
// let parent: any;
// class Parent extends Component {
// static components = { Child };
// static template = xml`
//
// parent:
//
//
`;
// state = state;
// constructor(a, b) {
// super(a, b);
// parent = this;
// }
// shouldUpdate() {
// return false;
// }
// }
// class App extends Component {
// static components = { Parent };
// static template = xml`
// `;
// }
// var div = document.createElement("div");
// fixture.appendChild(div);
// const app = new App();
// await app.mount(fixture);
// expect(fixture.innerHTML).toBe(
// "
"
// );
// 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(
// ""
// );
// });
// 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`
//
// child child:
//
`;
// state = state;
// shouldUpdate() {
// return shouldUpdate;
// }
// }
// class Child extends Component {
// static components = { ChildChild };
// static template = xml`
//
// child
//
//
`;
// shouldUpdate() {
// return shouldUpdate;
// }
// }
// let parent: any;
// class Parent extends Component {
// static components = { Child };
// static template = xml`
//
// parent:
//
//
`;
// state = state;
// constructor(a, b) {
// super(a, b);
// parent = this;
// }
// shouldUpdate() {
// return shouldUpdate;
// }
// }
// class App extends Component {
// static components = { Parent };
// static template = xml`
// `;
// }
// const app = new App();
// await app.mount(fixture);
// expect(fixture.innerHTML).toBe(
// ""
// );
// 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(
// ""
// );
// });
// });