");
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("
");
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`
");
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`
");
expect([
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:rendered",
]).toBeLogged();
defs[0].resolve(); // resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("
1b
"); // 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([
"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`
");
expect([
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
"ComponentD:willPatch",
"ComponentD:patched",
"ComponentC:patched",
"ComponentB: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`
");
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`
");
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`
");
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:willUpdateProps",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:mounted",
"Child:patched",
"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("
01
");
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("
123
");
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("
123
");
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(
"
111213
"
);
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("
123
");
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("
123
");
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("
123
");
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("
`;
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 = { fromA: 1 };
setup() {
useLogLifecycle();
}
}
const a = await mount(A, fixture);
expect(fixture.innerHTML).toBe("
123
");
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("
123
");
// trigger a re-rendering from C, which will remap its new fiber
c!.state.fromC += 10;
c!.render();
const prom = c!.render();
// trigger a re-rendering from B, which will remap its new fiber as well
b!.state.fromB += 10;
b!.render();
await nextTick();
// at this point, C rendering is still pending, and nothing should have been
// updated yet.
expect(fixture.innerHTML).toBe("
123
");
await prom;
expect(fixture.innerHTML).toBe(
"
111213
DDD
"
);
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willUpdateProps",
"C:willRender",
"C:rendered",
"D:setup",
"D:willStart",
"B:willRender",
"B:rendered",
"C:willUpdateProps",
"C:willRender",
"C:rendered",
"D:willDestroy",
"D:setup",
"D:willStart",
"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("
");
});
test("change state and call manually render: no unnecessary rendering", async () => {
let numberOfRender = 0;
class Test extends Component {
static template = xml`