import { App, Component, mount, onMounted, onWillStart, onWillUnmount, onWillUpdateProps, useState, } from "../../src"; import { Fiber } from "../../src/component/fibers"; import { Scheduler } from "../../src/component/scheduler"; import { status } from "../../src/component/status"; import { xml } from "../../src/tags"; 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) { 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("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("ab"); 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("
a2b2
"); 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("
a2b2
"); 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`
`; 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 new App(Parent).configure({ env }).mount(fixture); expect(fixture.innerHTML).toBe("
"); expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Parent:rendered", "Parent:mounted", ]).toBeLogged(); fixture.querySelector("button")!.click(); await nextTick(); expect(fixture.innerHTML).toBe("
Child
"); 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("

1b

"); 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("

1b

"); expect([ "ComponentB:willRender", "ComponentC:willUpdateProps", "ComponentB:rendered", ]).toBeLogged(); component.state.fromA = 2; await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); 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("

2c

"); 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("
1

1b

"); 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("
1

1b

"); expect([ "ComponentA:willRender", "ComponentB:willUpdateProps", "ComponentA:rendered", "ComponentB:willRender", "ComponentC:willUpdateProps", "ComponentB:rendered", ]).toBeLogged(); stateB.fromB = "c"; await nextTick(); expect(fixture.innerHTML).toBe("
1

1b

"); expect([ "ComponentB:willRender", "ComponentC:willUpdateProps", "ComponentB:rendered", ]).toBeLogged(); defs[1].resolve(); // resolve rendering initiated in B await nextTick(); expect(fixture.innerHTML).toBe("
2

2c

"); expect([ "ComponentC:willRender", "ComponentC:rendered", "ComponentA:willPatch", "ComponentB:willPatch", "ComponentC:willPatch", "ComponentC:patched", "ComponentB:patched", "ComponentA:patched", ]).toBeLogged(); defs[0].resolve(); // resolve rendering initiated in A await nextTick(); expect(fixture.innerHTML).toBe("
2

2c

"); 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("

1b

"); 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("

1b

"); expect([ "ComponentA:willRender", "ComponentB:willUpdateProps", "ComponentA:rendered", "ComponentB:willRender", "ComponentC:willUpdateProps", "ComponentB:rendered", ]).toBeLogged(); stateB.fromB = "c"; await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); 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("

2c

"); 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("

1c

"); 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("

1c

"); expect([ "ComponentA:willRender", "ComponentB:willUpdateProps", "ComponentA:rendered", ]).toBeLogged(); stateC.fromC = "d"; await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); expect([ "ComponentC:willRender", "ComponentD:willUpdateProps", "ComponentC:rendered", ]).toBeLogged(); defB.resolve(); // resolve rendering initiated in A (still blocked in D) await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); 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(ComponentD.prototype.someValue).toBeCalledTimes(1); expect(fixture.innerHTML).toBe("

1c

"); expect([]).toBeLogged(); defsD[1].resolve(); // completely resolve rendering initiated in A await nextTick(); expect(fixture.innerHTML).toBe("

2d

"); 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 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("

1c

"); 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("

1c

"); expect([ "ComponentA:willRender", "ComponentB:willUpdateProps", "ComponentA:rendered", ]).toBeLogged(); stateC.fromC = "d"; await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); expect([ "ComponentC:willRender", "ComponentD:willUpdateProps", "ComponentC:rendered", ]).toBeLogged(); defB.resolve(); // resolve rendering initiated in A (still blocked in D) await nextTick(); expect(fixture.innerHTML).toBe("

1c

"); 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("

2d

"); 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(); defsD[0].resolve(); // resolve rendering initiated in C (should be ignored) await nextTick(); expect(fixture.innerHTML).toBe("

2d

"); expect(ComponentD.prototype.someValue).toBeCalledTimes(2); expect([]).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("

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("

1

"); expect([ "ComponentA:willRender", "ComponentB:willUpdateProps", "ComponentA:rendered", ]).toBeLogged(); component.state.fromA = 3; await nextTick(); expect(fixture.innerHTML).toBe("

1

"); expect([ "ComponentA:willRender", "ComponentB:willUpdateProps", "ComponentA:rendered", ]).toBeLogged(); defsB[0].resolve(); // resolve first re-rendering (should be ignored) await nextTick(); expect(fixture.innerHTML).toBe("

1

"); expect(ComponentB.prototype.someValue).toBeCalledTimes(1); expect([]).toBeLogged(); defsB[1].resolve(); // resolve second re-rendering await nextTick(); expect(fixture.innerHTML).toBe("

3

"); 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("

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("

1

"); expect([ "ComponentA:willRender", "ComponentB:willUpdateProps", "ComponentA:rendered", ]).toBeLogged(); component.state.fromA = 3; await nextTick(); expect(fixture.innerHTML).toBe("

1

"); expect([ "ComponentA:willRender", "ComponentB:willUpdateProps", "ComponentA:rendered", ]).toBeLogged(); defsB[1].resolve(); // resolve second re-rendering await nextTick(); expect(fixture.innerHTML).toBe("

3

"); 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("

3

"); 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("

1b

"); 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("

2c

"); 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("

1b

"); 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("

1b

"); expect([ "ComponentA:willRender", "ComponentB:willUpdateProps", "ComponentA:rendered", ]).toBeLogged(); stateB.fromB = "c"; await nextTick(); expect(fixture.innerHTML).toBe("

1b

"); expect([]).toBeLogged(); def.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("

2c

"); 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("
a1a1

a1b1

"); 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("
a1a1

a1b1

"); 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("
a1a1

a1b1

"); expect([ "ComponentC:willRender", "ComponentD:willUpdateProps", "ComponentC:rendered", "ComponentD:willRender", "ComponentD:rendered", ]).toBeLogged(); def.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
a2a2

a2b2

"); 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`

`; 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("

2

"); 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: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("

123

"); expect(["C:willRender", "C:rendered"]).toBeLogged(); await nextTick(); expect(fixture.innerHTML).toBe( "

111213

" ); 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.skip("concurrent renderings scenario 16", async () => { let b: B | undefined = undefined; let c: C | undefined = undefined; class D extends Component { static template = xml`
    DDD
`; 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 = { 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("
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("
3
"); 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``; setup() { useLogLifecycle(); onMounted(() => parent.render()); onWillUnmount(() => parent.render()); } } // 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("
Panel1
"); expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Panel:setup", "Panel:willStart", "Parent:rendered", "Panel:willRender", "Panel:rendered", "Panel:mounted", "Parent:mounted", ]).toBeLogged(); parent.state.panel = "Panel2"; await nextTick(); expect(fixture.innerHTML).toBe("
Panel2
"); expect([ "Parent:willRender", "Panel:setup", "Panel:willStart", "Parent:rendered", "Panel:willRender", "Panel:rendered", "Parent:willPatch", "Panel:willUnmount", "Panel:willDestroy", "Panel:mounted", "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", ]).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.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( // "
parent: 1
child
child child: 10
" // ); // 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( // "
parent: 2
child
child child: 11
" // ); // }); // 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( // "
parent: 1
child
child child: 10
" // ); // 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( // "
parent: 2
child
child child: 11
" // ); // }); // });