From dcefd26bee3b25c3189e66db6f62d5c9d663ee30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sat, 27 Nov 2021 10:08:10 +0100 Subject: [PATCH] [REF] tests: improve useLogLifecycle and helpers --- package.json | 1 + tests/components/concurrency.test.ts | 1398 +++++++++++++------------- tests/components/lifecycle.test.ts | 300 +++--- tests/components/t_component.test.ts | 35 +- tests/components/t_foreach.test.ts | 10 +- tests/helpers.ts | 79 +- tests/reactivity.test.ts | 2 +- 7 files changed, 899 insertions(+), 926 deletions(-) diff --git a/package.json b/package.json index ac2822ed..c55b101a 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "git-rev-sync": "^1.12.0", "github-api": "^3.3.0", "jest": "^27.1.0", + "jest-diff": "^27.3.1", "jest-environment-jsdom": "^27.1.0", "live-server": "^1.2.1", "npm-run-all": "^4.1.5", diff --git a/tests/components/concurrency.test.ts b/tests/components/concurrency.test.ts index 4512ebae..d7f684dc 100644 --- a/tests/components/concurrency.test.ts +++ b/tests/components/concurrency.test.ts @@ -1,11 +1,14 @@ -import { App, Component, mount, onWillStart, onWillUpdateProps, useState } from "../../src"; -import { Fiber } from "../../src/component/fibers"; import { + App, + Component, + mount, onMounted, - onPatched, - onWillPatch, + onWillStart, onWillUnmount, -} from "../../src/component/lifecycle_hooks"; + 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"; @@ -42,13 +45,12 @@ afterEach(() => { describe("async rendering", () => { test("destroying a widget before start is over", async () => { - let steps: string[] = []; let def = makeDeferred(); let w: any = null; class W extends Component { static template = xml`
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); expect(status(this)).toBe("new"); w = this; onWillStart(() => def); @@ -62,18 +64,17 @@ describe("async rendering", () => { def.resolve(); await nextTick(); expect(status(w)).toBe("destroyed"); - expect(steps).toEqual(["W:setup", "W:willStart", "W:destroyed"]); + expect(["W:setup", "W:willStart", "W:destroyed"]).toBeLogged(); }); }); test("destroying/recreating a subwidget with different props (if start is not over)", async () => { - let steps: string[] = []; let def = makeDeferred(); let n = 0; class Child extends Component { static template = xml`child:`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); n++; onWillStart(() => def); } @@ -87,12 +88,14 @@ test("destroying/recreating a subwidget with different props (if start is not ov static components = { Child }; state = useState({ val: 1 }); setup() { - useLogLifecycle(steps); + 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; @@ -100,42 +103,35 @@ test("destroying/recreating a subwidget with different props (if start is not ov 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); - def.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("
child:3
"); - expect(Object.values(w.__owl__.children).length).toBe(1); - expect(steps).toEqual([ - "W:setup", - "W:willStart", - "W:willRender", - "W:rendered", - "W:mounted", - "W:willRender", - "Child:setup", - "Child:willStart", - "W:rendered", + expect([ "W:willRender", "Child:destroyed", "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", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("creating two async components, scenario 1", async () => { - let steps: string[] = []; - let defA = makeDeferred(); let defB = makeDeferred(); let nbRenderings: number = 0; @@ -144,7 +140,7 @@ test("creating two async components, scenario 1", async () => { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillStart(() => defA); } @@ -157,7 +153,7 @@ test("creating two async components, scenario 1", async () => { class ChildB extends Component { static template = xml`b`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillStart(() => defB); } } @@ -170,36 +166,29 @@ test("creating two async components, scenario 1", async () => { static components = { ChildA, ChildB }; state = useState({ flagA: false, flagB: false }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - expect(fixture.innerHTML).toBe(""); - parent.state.flagA = true; - await nextTick(); - expect(fixture.innerHTML).toBe(""); - parent.state.flagB = true; - await nextTick(); - expect(fixture.innerHTML).toBe(""); - defB.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe(""); - expect(nbRenderings).toBe(0); - defA.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("ab"); - expect(nbRenderings).toBe(1); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Parent:rendered", "Parent:mounted", - "Parent:willRender", - "ChildA:setup", - "ChildA:willStart", - "Parent:rendered", + ]).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:destroyed", "ChildA:setup", @@ -207,20 +196,29 @@ test("creating two async components, scenario 1", async () => { "ChildB:setup", "ChildB:willStart", "Parent:rendered", - "ChildB:willRender", - "ChildB: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", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("creating two async components, scenario 2", async () => { - let steps: string[] = []; let defA = makeDeferred(); let defB = makeDeferred(); @@ -228,7 +226,7 @@ test("creating two async components, scenario 2", async () => { static template = xml`a`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defA); } } @@ -236,7 +234,7 @@ test("creating two async components, scenario 2", async () => { class ChildB extends Component { static template = xml`b`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillStart(() => defB); } } @@ -250,26 +248,11 @@ test("creating two async components, scenario 2", async () => { static components = { ChildA, ChildB }; state = useState({ valA: 1, valB: 2, flagB: false }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - - expect(fixture.innerHTML).toBe("
a1
"); - parent.state.valA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("
a1
"); - parent.state.flagB = true; - await nextTick(); - expect(fixture.innerHTML).toBe("
a1
"); - defB.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("
a1
"); - defA.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("
a2b2
"); - - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -280,16 +263,34 @@ test("creating two async components, scenario 2", async () => { "ChildA:rendered", "ChildA:mounted", "Parent:mounted", - "Parent:willRender", - "ChildA:willUpdateProps", - "Parent:rendered", + ]).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", - "ChildB:willRender", - "ChildB: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", @@ -297,26 +298,24 @@ test("creating two async components, scenario 2", async () => { "ChildB:mounted", "ChildA:patched", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("creating two async components, scenario 3 (patching in the same frame)", async () => { - let steps: string[] = []; let defA = makeDeferred(); let defB = makeDeferred(); class ChildA extends Component { static template = xml`a`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defA); } } class ChildB extends Component { static template = xml`b`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillStart(() => defB); } } @@ -330,25 +329,11 @@ test("creating two async components, scenario 3 (patching in the same frame)", a static components = { ChildA, ChildB }; state = useState({ valA: 1, valB: 2, flagB: false }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - - expect(fixture.innerHTML).toBe("
a1
"); - parent.state.valA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("
a1
"); - parent.state.flagB = true; - await nextTick(); - expect(fixture.innerHTML).toBe("
a1
"); - defB.resolve(); - expect(fixture.innerHTML).toBe("
a1
"); - defA.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("
a2b2
"); - - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -359,14 +344,33 @@ test("creating two async components, scenario 3 (patching in the same frame)", a "ChildA:rendered", "ChildA:mounted", "Parent:mounted", - "Parent:willRender", - "ChildA:willUpdateProps", - "Parent:rendered", + ]).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", @@ -376,19 +380,17 @@ test("creating two async components, scenario 3 (patching in the same frame)", a "ChildB:mounted", "ChildA:patched", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("update a sub-component twice in the same frame", async () => { - const steps: string[] = []; const defs = [makeDeferred(), makeDeferred()]; let index = 0; class ChildA extends Component { static template = xml``; setup() { onWillUpdateProps(() => defs[index++]); - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -397,24 +399,12 @@ test("update a sub-component twice in the same frame", async () => { static components = { ChildA }; state = useState({ valA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - expect(fixture.innerHTML).toBe("
1
"); - parent.state.valA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("
1
"); - parent.state.valA = 3; - await nextTick(); - expect(fixture.innerHTML).toBe("
1
"); - defs[0].resolve(); - await Promise.resolve(); - defs[1].resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("
3
"); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -425,33 +415,42 @@ test("update a sub-component twice in the same frame", async () => { "ChildA:rendered", "ChildA:mounted", "Parent:mounted", - "Parent:willRender", - "ChildA:willUpdateProps", - "Parent:rendered", - "Parent:willRender", - "ChildA:willUpdateProps", - "Parent:rendered", + ]).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", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("update a sub-component twice in the same frame, 2", async () => { - const steps: string[] = []; class ChildA extends Component { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } val() { - steps.push("render"); return this.props.val; } } @@ -461,16 +460,11 @@ test("update a sub-component twice in the same frame, 2", async () => { static components = { ChildA }; state = useState({ valA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - - expect(fixture.innerHTML).toBe("
1
"); - parent.state.valA = 2; - await nextMicroTick(); - await nextMicroTick(); - expect(steps.splice(0)).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -478,64 +472,48 @@ test("update a sub-component twice in the same frame, 2", async () => { "ChildA:willStart", "Parent:rendered", "ChildA:willRender", - "render", "ChildA:rendered", "ChildA:mounted", "Parent:mounted", - "Parent:willRender", - "ChildA:willUpdateProps", - "Parent:rendered", - ]); + ]).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(steps.splice(0)).toEqual(["ChildA:willRender", "render", "ChildA:rendered"]); + expect(["ChildA:willRender", "ChildA:rendered"]).toBeLogged(); expect(fixture.innerHTML).toBe("
1
"); parent.state.valA = 3; await nextMicroTick(); await nextMicroTick(); - expect(steps.splice(0)).toEqual([ - "Parent:willRender", - "ChildA:willUpdateProps", - "Parent:rendered", - ]); + expect(["Parent:willRender", "ChildA:willUpdateProps", "Parent:rendered"]).toBeLogged(); + await nextMicroTick(); // same as above await nextMicroTick(); - expect(steps).toEqual(["ChildA:willRender", "render", "ChildA:rendered"]); + expect(["ChildA:willRender", "ChildA:rendered"]).toBeLogged(); expect(fixture.innerHTML).toBe("
1
"); + await nextTick(); expect(fixture.innerHTML).toBe("
3
"); - expect(steps).toEqual([ - "ChildA:willRender", - "render", - "ChildA:rendered", - "Parent:willPatch", - "ChildA:willPatch", - "ChildA:patched", - "Parent:patched", - ]); - Object.freeze(steps); + expect(["Parent:willPatch", "ChildA:willPatch", "ChildA:patched", "Parent:patched"]).toBeLogged(); }); test("properly behave when destroyed/unmounted while rendering ", async () => { - const steps: string[] = []; const def = makeDeferred(); class SubChild extends Component { static template = xml`
`; setup() { - useLogLifecycle(steps); - onWillPatch(() => { - throw new Error("Should not happen!"); - }); - onPatched(() => { - throw new Error("Should not happen!"); - }); + useLogLifecycle(); onWillUpdateProps(() => { return def; }); @@ -546,7 +524,7 @@ test("properly behave when destroyed/unmounted while rendering ", async () => { static template = xml`
`; static components = { SubChild }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -556,30 +534,13 @@ test("properly behave when destroyed/unmounted while rendering ", async () => { static components = { Child }; state = useState({ flag: true, val: "Framboise Lindemans" }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
"); - - // 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("
"); - - // 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("
"); - - // we now resolve def, so the child rendering is now complete. - def.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("
"); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -595,12 +556,28 @@ test("properly behave when destroyed/unmounted while rendering ", async () => { "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", @@ -609,17 +586,20 @@ test("properly behave when destroyed/unmounted while rendering ", async () => { "SubChild:destroyed", "Child:destroyed", "Parent:patched", - ]); - Object.freeze(steps); + ]).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 () => { - const steps: string[] = []; - class Child extends Component { static template = xml`
Child
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -632,7 +612,7 @@ test("rendering component again in next microtick", async () => { static components = { Child }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } async onClick() { this.env.config.flag = true; @@ -645,16 +625,18 @@ test("rendering component again in next microtick", async () => { const env = { config: { flag: false } }; await new App(Parent).configure({ env }).mount(fixture); expect(fixture.innerHTML).toBe("
"); - fixture.querySelector("button")!.click(); - await nextTick(); - expect(fixture.innerHTML).toBe("
Child
"); - - expect(steps).toEqual([ + 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", @@ -669,19 +651,17 @@ test("rendering component again in next microtick", async () => { "Parent:willPatch", "Child:mounted", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 1", async () => { - const steps: string[] = []; const def = makeDeferred(); let stateB: any = null; class ComponentC extends Component { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => def); } someValue() { @@ -697,7 +677,7 @@ test("concurrent renderings scenario 1", async () => { setup() { stateB = this.state; - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -706,31 +686,13 @@ test("concurrent renderings scenario 1", async () => { static components = { ComponentB }; state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const component = await mount(ComponentA, fixture); - expect(fixture.innerHTML).toBe("

1b

"); - - stateB.fromB = "c"; - await nextTick(); - - expect(fixture.innerHTML).toBe("

1b

"); - - component.state.fromA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("

1b

"); - - expect(ComponentC.prototype.someValue).toBeCalledTimes(1); - def.resolve(); - await nextTick(); - - expect(fixture.innerHTML).toBe("

2c

"); - expect(ComponentC.prototype.someValue).toBeCalledTimes(2); - - expect(steps).toEqual([ + expect([ "ComponentA:setup", "ComponentA:willStart", "ComponentA:willRender", @@ -746,15 +708,35 @@ test("concurrent renderings scenario 1", async () => { "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", @@ -763,12 +745,10 @@ test("concurrent renderings scenario 1", async () => { "ComponentC:patched", "ComponentB:patched", "ComponentA:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 2", async () => { - const steps: string[] = []; // 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()]; @@ -778,7 +758,7 @@ test("concurrent renderings scenario 2", async () => { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defs[index++]); } } @@ -789,7 +769,7 @@ test("concurrent renderings scenario 2", async () => { state = useState({ fromB: "b" }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); stateB = this.state; } } @@ -799,30 +779,13 @@ test("concurrent renderings scenario 2", async () => { static components = { ComponentB }; state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const component = await mount(ComponentA, fixture); - expect(fixture.innerHTML).toBe("
1

1b

"); - - component.state.fromA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("
1

1b

"); - - stateB.fromB = "c"; - await nextTick(); - expect(fixture.innerHTML).toBe("
1

1b

"); - - defs[1].resolve(); // resolve rendering initiated in B - await nextTick(); - expect(fixture.innerHTML).toBe("
2

2c

"); - - defs[0].resolve(); // resolve rendering initiated in A - await nextTick(); - expect(fixture.innerHTML).toBe("
2

2c

"); - expect(steps).toEqual([ + expect([ "ComponentA:setup", "ComponentA:willStart", "ComponentA:willRender", @@ -838,15 +801,33 @@ test("concurrent renderings scenario 2", async () => { "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", @@ -855,12 +836,15 @@ test("concurrent renderings scenario 2", async () => { "ComponentC:patched", "ComponentB:patched", "ComponentA:patched", - ]); - Object.freeze(steps); + ]).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 steps: string[] = []; const defs = [makeDeferred(), makeDeferred()]; let index = 0; let stateB: any = null; @@ -868,7 +852,7 @@ test("concurrent renderings scenario 2bis", async () => { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defs[index++]); } } @@ -879,7 +863,7 @@ test("concurrent renderings scenario 2bis", async () => { state = useState({ fromB: "b" }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); stateB = this.state; } } @@ -890,30 +874,13 @@ test("concurrent renderings scenario 2bis", async () => { state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const component = await mount(ComponentA, fixture); - expect(fixture.innerHTML).toBe("

1b

"); - - component.state.fromA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("

1b

"); - - stateB.fromB = "c"; - await nextTick(); - expect(fixture.innerHTML).toBe("

1b

"); - - 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 - - defs[1].resolve(); // resolve rendering initiated in B - await nextTick(); - expect(fixture.innerHTML).toBe("

2c

"); - expect(steps).toEqual([ + expect([ "ComponentA:setup", "ComponentA:willStart", "ComponentA:willRender", @@ -929,15 +896,38 @@ test("concurrent renderings scenario 2bis", async () => { "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", @@ -946,12 +936,10 @@ test("concurrent renderings scenario 2bis", async () => { "ComponentC:patched", "ComponentB:patched", "ComponentA:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 3", async () => { - const steps: string[] = []; const defB = makeDeferred(); const defsD = [makeDeferred(), makeDeferred()]; let index = 0; @@ -961,7 +949,7 @@ test("concurrent renderings scenario 3", async () => { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defsD[index++]); } someValue() { @@ -975,7 +963,7 @@ test("concurrent renderings scenario 3", async () => { static components = { ComponentD }; state = useState({ fromC: "c" }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); stateC = this.state; } } @@ -985,7 +973,7 @@ test("concurrent renderings scenario 3", async () => { static components = { ComponentC }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defB); } } @@ -996,69 +984,76 @@ test("concurrent renderings scenario 3", async () => { state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + 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(steps).toEqual([ - "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", - "ComponentA:willRender", - "ComponentB:willUpdateProps", - "ComponentA:rendered", - "ComponentC:willRender", - "ComponentD:willUpdateProps", - "ComponentC:rendered", - "ComponentB:willRender", - "ComponentC:willUpdateProps", - "ComponentB:rendered", - "ComponentC:willRender", - "ComponentD:willUpdateProps", - "ComponentC:rendered", + expect([ "ComponentD:willRender", "ComponentD:rendered", "ComponentA:willPatch", @@ -1069,12 +1064,10 @@ test("concurrent renderings scenario 3", async () => { "ComponentC:patched", "ComponentB:patched", "ComponentA:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 4", async () => { - const steps: string[] = []; const defB = makeDeferred(); const defsD = [makeDeferred(), makeDeferred()]; let index = 0; @@ -1084,7 +1077,7 @@ test("concurrent renderings scenario 4", async () => { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defsD[index++]); } someValue() { @@ -1098,7 +1091,7 @@ test("concurrent renderings scenario 4", async () => { static components = { ComponentD }; state = useState({ fromC: "c" }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); stateC = this.state; } } @@ -1108,7 +1101,7 @@ test("concurrent renderings scenario 4", async () => { static components = { ComponentC }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defB); } } @@ -1119,37 +1112,13 @@ test("concurrent renderings scenario 4", async () => { state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const component = await mount(ComponentA, fixture); - expect(fixture.innerHTML).toBe("

1c

"); - - component.state.fromA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("

1c

"); - - stateC.fromC = "d"; - await nextTick(); - expect(fixture.innerHTML).toBe("

1c

"); - - defB.resolve(); // resolve rendering initiated in A (still blocked in D) - await nextTick(); - expect(fixture.innerHTML).toBe("

1c

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

2d

"); - expect(ComponentD.prototype.someValue).toBeCalledTimes(2); - - 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(steps).toEqual([ + expect([ "ComponentA:setup", "ComponentA:willStart", "ComponentA:willRender", @@ -1170,18 +1139,43 @@ test("concurrent renderings scenario 4", async () => { "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", @@ -1192,12 +1186,16 @@ test("concurrent renderings scenario 4", async () => { "ComponentC:patched", "ComponentB:patched", "ComponentA:patched", - ]); - Object.freeze(steps); + ]).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 steps: string[] = []; const defsB = [makeDeferred(), makeDeferred()]; let index = 0; @@ -1205,7 +1203,7 @@ test("concurrent renderings scenario 5", async () => { static template = xml`

`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defsB[index++]); } someValue() { @@ -1219,60 +1217,64 @@ test("concurrent renderings scenario 5", async () => { static template = xml`
`; state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + 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(steps).toEqual([ - "ComponentA:setup", - "ComponentA:willStart", - "ComponentA:willRender", - "ComponentB:setup", - "ComponentB:willStart", - "ComponentA:rendered", - "ComponentB:willRender", - "ComponentB:rendered", - "ComponentB:mounted", - "ComponentA:mounted", - "ComponentA:willRender", - "ComponentB:willUpdateProps", - "ComponentA:rendered", - "ComponentA:willRender", - "ComponentB:willUpdateProps", - "ComponentA:rendered", + expect([ "ComponentB:willRender", "ComponentB:rendered", "ComponentA:willPatch", "ComponentB:willPatch", "ComponentB:patched", "ComponentA:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 6", async () => { - const steps: string[] = []; const defsB = [makeDeferred(), makeDeferred()]; let index = 0; @@ -1280,7 +1282,7 @@ test("concurrent renderings scenario 6", async () => { static template = xml`

`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => defsB[index++]); } someValue() { @@ -1295,32 +1297,13 @@ test("concurrent renderings scenario 6", async () => { state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const component = await mount(ComponentA, fixture); - expect(fixture.innerHTML).toBe("

1

"); - - component.state.fromA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("

1

"); - - component.state.fromA = 3; - await nextTick(); - expect(fixture.innerHTML).toBe("

1

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

3

"); - expect(ComponentB.prototype.someValue).toBeCalledTimes(2); - - defsB[0].resolve(); // resolve first re-rendering (should be ignored) - await nextTick(); - expect(fixture.innerHTML).toBe("

3

"); - expect(ComponentB.prototype.someValue).toBeCalledTimes(2); - expect(steps).toEqual([ + expect([ "ComponentA:setup", "ComponentA:willStart", "ComponentA:willRender", @@ -1331,31 +1314,53 @@ test("concurrent renderings scenario 6", async () => { "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", - ]); - Object.freeze(steps); + ]).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 () => { - const steps: string[] = []; - class ComponentB extends Component { static template = xml`

`; state = useState({ fromB: "b" }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => { this.state.fromB = "c"; }); @@ -1371,20 +1376,14 @@ test("concurrent renderings scenario 7", async () => { static template = xml`
`; state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const component = await mount(ComponentA, fixture); - expect(fixture.innerHTML).toBe("

1b

"); expect(ComponentB.prototype.someValue).toBeCalledTimes(1); - - component.state.fromA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("

2c

"); - expect(ComponentB.prototype.someValue).toBeCalledTimes(2); - expect(steps).toEqual([ + expect([ "ComponentA:setup", "ComponentA:willStart", "ComponentA:willRender", @@ -1395,6 +1394,13 @@ test("concurrent renderings scenario 7", async () => { "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", @@ -1404,20 +1410,17 @@ test("concurrent renderings scenario 7", async () => { "ComponentB:willPatch", "ComponentB:patched", "ComponentA:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 8", async () => { - const steps: string[] = []; - const def = makeDeferred(); let stateB: any = null; class ComponentB extends Component { static template = xml`

`; state = useState({ fromB: "b" }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); stateB = this.state; onWillUpdateProps(() => def); } @@ -1428,27 +1431,13 @@ test("concurrent renderings scenario 8", async () => { static template = xml`
`; state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const component = await mount(ComponentA, fixture); - expect(fixture.innerHTML).toBe("

1b

"); - - component.state.fromA = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("

1b

"); - - stateB.fromB = "c"; - await nextTick(); - expect(fixture.innerHTML).toBe("

1b

"); - - def.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("

2c

"); - - expect(steps).toEqual([ + expect([ "ComponentA:setup", "ComponentA:willStart", "ComponentA:willRender", @@ -1459,17 +1448,33 @@ test("concurrent renderings scenario 8", async () => { "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", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 9", async () => { @@ -1485,14 +1490,13 @@ test("concurrent renderings scenario 9", async () => { // 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 steps: string[] = []; const def = makeDeferred(); let stateC: any = null; class ComponentD extends Component { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -1503,7 +1507,7 @@ test("concurrent renderings scenario 9", async () => { setup() { stateC = this.state; - useLogLifecycle(steps); + useLogLifecycle(); } } class ComponentB extends Component { @@ -1511,7 +1515,7 @@ test("concurrent renderings scenario 9", async () => { setup() { onWillUpdateProps(() => def); - useLogLifecycle(steps); + useLogLifecycle(); } } class ComponentA extends Component { @@ -1524,27 +1528,13 @@ test("concurrent renderings scenario 9", async () => { static components = { ComponentB, ComponentC }; state = useState({ fromA: "a1" }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const component = await mount(ComponentA, fixture); - expect(fixture.innerHTML).toBe("
a1a1

a1b1

"); - - component.state.fromA = "a2"; - await nextTick(); - expect(fixture.innerHTML).toBe("
a1a1

a1b1

"); - - stateC.fromC = "b2"; - await nextTick(); - expect(fixture.innerHTML).toBe("
a1a1

a1b1

"); - - def.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("
a2a2

a2b2

"); - - expect(steps).toEqual([ + expect([ "ComponentA:setup", "ComponentA:willStart", "ComponentA:willRender", @@ -1565,6 +1555,12 @@ test("concurrent renderings scenario 9", async () => { "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", @@ -1574,11 +1570,23 @@ test("concurrent renderings scenario 9", async () => { "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", @@ -1589,8 +1597,7 @@ test("concurrent renderings scenario 9", async () => { "ComponentC:patched", "ComponentB:patched", "ComponentA:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 10", async () => { @@ -1609,7 +1616,6 @@ test("concurrent renderings scenario 10", async () => { // 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 steps: string[] = []; const defB = makeDeferred(); const defC = makeDeferred(); let stateB: any = null; @@ -1617,7 +1623,7 @@ test("concurrent renderings scenario 10", async () => { class ComponentC extends Component { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillStart(() => defC); } get value() { @@ -1631,7 +1637,7 @@ test("concurrent renderings scenario 10", async () => { state = useState({ hasChild: false }); static components = { ComponentC }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); stateB = this.state; onWillUpdateProps(() => defB); } @@ -1643,26 +1649,13 @@ test("concurrent renderings scenario 10", async () => { state = useState({ value: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const componentA = await mount(ComponentA, fixture); expect(fixture.innerHTML).toBe("

"); - stateB.hasChild = true; - await nextTick(); - expect(fixture.innerHTML).toBe("

"); - - componentA.state.value = 2; - defC.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("

"); - - defB.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("

2

"); - expect(rendered).toBe(1); - expect(steps).toEqual([ + expect([ "ComponentA:setup", "ComponentA:willStart", "ComponentA:willRender", @@ -1673,13 +1666,33 @@ test("concurrent renderings scenario 10", async () => { "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:destroyed", "ComponentC:setup", @@ -1692,8 +1705,7 @@ test("concurrent renderings scenario 10", async () => { "ComponentC:mounted", "ComponentB:patched", "ComponentA:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 11", async () => { @@ -1702,7 +1714,6 @@ test("concurrent renderings scenario 11", async () => { // 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 steps: string[] = []; const def = makeDeferred(); let child: any = null; class Child extends Component { @@ -1710,7 +1721,7 @@ test("concurrent renderings scenario 11", async () => { val = 3; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => { child = this; return def; @@ -1723,22 +1734,12 @@ test("concurrent renderings scenario 11", async () => { static components = { Child }; state = useState({ valA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
1|3
"); - parent.state.valA = 2; - - await nextTick(); - setTimeout(() => { - def.resolve(); - }, 20); - child.val = 5; - await child.render(); - expect(fixture.innerHTML).toBe("
2|5
"); - - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -1749,6 +1750,17 @@ test("concurrent renderings scenario 11", async () => { "Child:rendered", "Child:mounted", "Parent:mounted", + ]).toBeLogged(); + + parent.state.valA = 2; + await nextTick(); + setTimeout(() => { + def.resolve(); + }, 20); + child.val = 5; + await child.render(); + expect(fixture.innerHTML).toBe("
2|5
"); + expect([ "Parent:willRender", "Child:willUpdateProps", "Parent:rendered", @@ -1758,8 +1770,7 @@ test("concurrent renderings scenario 11", async () => { "Child:willPatch", "Child:patched", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 12", async () => { @@ -1769,13 +1780,12 @@ test("concurrent renderings scenario 12", async () => { // 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 steps: string[] = []; const def = makeDeferred(); class Child extends Component { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => def); } } @@ -1787,7 +1797,7 @@ test("concurrent renderings scenario 12", async () => { static components = { Child }; state = useState({ val: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } get val() { @@ -1799,23 +1809,7 @@ test("concurrent renderings scenario 12", async () => { const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
1
"); expect(rendered).toBe(1); - - parent.state.val = 2; - await nextTick(); - expect(fixture.innerHTML).toBe("
1
"); - expect(rendered).toBe(2); - - parent.state.val = 3; - parent.state.val = 4; - await nextTick(); - expect(fixture.innerHTML).toBe("
1
"); - expect(rendered).toBe(3); - - def.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe("
4
"); - expect(rendered).toBe(3); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -1826,31 +1820,43 @@ test("concurrent renderings scenario 12", async () => { "Child:rendered", "Child:mounted", "Parent:mounted", - "Parent:willRender", - "Child:willUpdateProps", - "Parent:rendered", - "Parent:willRender", - "Child:willUpdateProps", - "Parent:rendered", + ]).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", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 13", async () => { - const steps: string[] = []; let lastChild: any = null; class Child extends Component { static template = xml``; state = useState({ val: 0 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); onMounted(() => { if (lastChild) { lastChild.state.val = 0; @@ -1870,21 +1876,13 @@ test("concurrent renderings scenario 13", async () => { static components = { Child }; state = useState({ bool: false }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
0
"); - - await nextTick(); // wait for changes triggered in mounted to be applied - expect(fixture.innerHTML).toBe("
1
"); - - parent.state.bool = true; - await nextTick(); // wait for this change to be applied - await nextTick(); // wait for changes triggered in mounted to be applied - expect(fixture.innerHTML).toBe("
01
"); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -1897,8 +1895,15 @@ test("concurrent renderings scenario 13", async () => { "Parent:mounted", "Child:willRender", "Child:rendered", - "Child:willPatch", - "Child:patched", + ]).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", @@ -1917,16 +1922,14 @@ test("concurrent renderings scenario 13", async () => { "Child:rendered", "Child:willRender", "Child:rendered", - "Child:willPatch", - "Child:patched", - "Child:willPatch", - "Child:patched", - ]); - Object.freeze(steps); + ]).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 () => { - const steps: string[] = []; let b: B | undefined = undefined; let c: C | undefined = undefined; class C extends Component { @@ -1939,7 +1942,7 @@ test("concurrent renderings scenario 14", async () => { state = useState({ fromC: 3 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); c = this; } } @@ -1947,7 +1950,7 @@ test("concurrent renderings scenario 14", async () => { static template = xml`

`; static components = { C }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); b = this; } state = useState({ fromB: 2 }); @@ -1959,35 +1962,12 @@ test("concurrent renderings scenario 14", async () => { state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const a = await mount(A, fixture); expect(fixture.innerHTML).toBe("

123

"); - - // 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; - // 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(steps).toEqual([ + expect([ "A:setup", "A:willStart", "A:willRender", @@ -2003,12 +1983,38 @@ test("concurrent renderings scenario 14", async () => { "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", @@ -2022,12 +2028,10 @@ test("concurrent renderings scenario 14", async () => { "C:patched", "B:patched", "A:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("concurrent renderings scenario 15", async () => { - const steps: string[] = []; let b: B | undefined = undefined; let c: C | undefined = undefined; class C extends Component { @@ -2040,7 +2044,7 @@ test("concurrent renderings scenario 15", async () => { state = useState({ fromC: 3 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); c = this; } } @@ -2048,7 +2052,7 @@ test("concurrent renderings scenario 15", async () => { static template = xml`

`; static components = { C }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); b = this; } state = useState({ fromB: 2 }); @@ -2058,12 +2062,29 @@ test("concurrent renderings scenario 15", async () => { static components = { B }; state = useState({ fromA: 1 }); setup() { - useLogLifecycle(steps); + 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; @@ -2074,6 +2095,14 @@ test("concurrent renderings scenario 15", async () => { 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; @@ -2084,40 +2113,19 @@ test("concurrent renderings scenario 15", async () => { // 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(steps).toEqual([ - "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", - "A:willRender", - "B:willUpdateProps", - "A:rendered", - "B:willRender", - "C:willUpdateProps", - "B:rendered", - "C:willRender", - "C:rendered", + expect([ "B:willRender", "C:willUpdateProps", "B:rendered", @@ -2129,19 +2137,17 @@ test("concurrent renderings scenario 15", async () => { "C:patched", "B:patched", "A:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test.skip("concurrent renderings scenario 16", async () => { - const steps: string[] = []; let b: B | undefined = undefined; let c: C | undefined = undefined; class D extends Component { static template = xml`
    DDD
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillStart(async () => { await nextTick(); await nextTick(); @@ -2159,7 +2165,7 @@ test.skip("concurrent renderings scenario 16", async () => { static components = { D }; state = { fromC: 3 }; // not reactive setup() { - useLogLifecycle(steps); + useLogLifecycle(); c = this; } } @@ -2167,7 +2173,7 @@ test.skip("concurrent renderings scenario 16", async () => { static template = xml`

`; static components = { C }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); b = this; } state = { fromB: 2 }; @@ -2178,11 +2184,28 @@ test.skip("concurrent renderings scenario 16", async () => { state = { fromA: 1 }; setup() { - useLogLifecycle(steps); + 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; @@ -2210,22 +2233,7 @@ test.skip("concurrent renderings scenario 16", async () => { expect(fixture.innerHTML).toBe( "

111213

    DDD

" ); - expect(steps).toEqual([ - "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", + expect([ "A:willRender", "B:willUpdateProps", "A:rendered", @@ -2253,35 +2261,10 @@ test.skip("concurrent renderings scenario 16", async () => { "C:patched", "B:patched", "A:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); -// test.skip("concurrent renderings scenario 17", async () => { -// class Parent extends Component { -// static template = xml``; -// state = useState({ value: 1 }); -// } - -// const parent = await mount(Parent, fixture); -// expect(fixture.innerHTML).toBe("1"); - -// parent.state.value = 2; -// parent.__owl__.fiber!.cancel(); - -// parent.state.value = 3; // update value directly -// await nextTick(); -// expect(fixture.innerHTML).toBe("3"); - -// parent.state.value = 4; // update value after a tick -// await nextTick(); -// expect(fixture.innerHTML).toBe("4"); -// }); - -// TODO: unskip when t-key is reimplemented properly test("calling render in destroy", async () => { - const steps: any[] = []; - let a: any = null; let c: any = null; @@ -2298,9 +2281,9 @@ test("calling render in destroy", async () => { static components = { C }; setup() { + useLogLifecycle(); c = this; onMounted(() => { - steps.push("B:mounted"); if (flag) { this.render(); } else { @@ -2309,7 +2292,6 @@ test("calling render in destroy", async () => { }); onWillUnmount(() => { - steps.push("B:willUnmount"); c.render(); }); } @@ -2329,19 +2311,30 @@ test("calling render in destroy", async () => { 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; await a.render(); // this nextTick is critical, otherwise jest may silently swallow errors await nextTick(); - - expect(steps).toStrictEqual(["B:mounted", "B:willUnmount", "B:mounted"]); + expect([ + "B:setup", + "B:willStart", + "B:willRender", + "B:rendered", + "B:willUnmount", + "B:destroyed", + "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 () => { - const steps: string[] = []; let numberOfRender = 0; class Test extends Component { @@ -2349,7 +2342,7 @@ test("change state and call manually render: no unnecessary rendering", async () state = useState({ val: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } get value() { numberOfRender++; @@ -2358,6 +2351,13 @@ test("change state and call manually render: no unnecessary rendering", async () } 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); @@ -2365,29 +2365,17 @@ test("change state and call manually render: no unnecessary rendering", async () await test.render(); expect(fixture.innerHTML).toBe("
2
"); expect(numberOfRender).toBe(2); - expect(steps).toEqual([ - "Test:setup", - "Test:willStart", - "Test:willRender", - "Test:rendered", - "Test:mounted", - "Test:willRender", - "Test:rendered", - "Test:willPatch", - "Test:patched", - ]); - Object.freeze(steps); + expect(["Test:willRender", "Test:rendered", "Test:willPatch", "Test:patched"]).toBeLogged(); }); test("changing state before first render does not trigger a render", async () => { - const steps: string[] = []; let renders = 0; class TestW extends Component { static template = xml`
`; state = useState({ drinks: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); this.state.drinks++; onWillStart(() => { this.state.drinks++; @@ -2399,29 +2387,28 @@ test("changing state before first render does not trigger a render", async () => } } await mount(TestW, fixture); - - await nextTick(); - expect(renders).toBe(1); - expect(fixture.innerHTML).toBe("
3
"); - expect(steps).toEqual([ + expect([ "TestW:setup", "TestW:willStart", "TestW:willRender", "TestW:rendered", "TestW:mounted", - ]); - Object.freeze(steps); + ]).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 () => { - const steps: string[] = []; let renders = 0; class TestW extends Component { static template = xml`
`; state = useState({ drinks: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); this.state.drinks++; onWillStart(() => { this.state.drinks++; @@ -2437,27 +2424,26 @@ test("changing state before first render does not trigger a render (with parent) static components = { TestW }; static template = xml`
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } state = useState({ flag: false }); } const parent = await mount(Parent, fixture); - expect(fixture.innerHTML).toBe("
"); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Parent:rendered", "Parent:mounted", - ]); - steps.splice(0); + ]).toBeLogged(); + parent.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
3
"); expect(renders).toBe(1); - expect(steps).toEqual([ + expect([ "Parent:willRender", "TestW:setup", "TestW:willStart", @@ -2467,8 +2453,7 @@ test("changing state before first render does not trigger a render (with parent) "Parent:willPatch", "TestW:mounted", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("render method wait until rendering is done", async () => { @@ -2489,12 +2474,11 @@ test("render method wait until rendering is done", async () => { test("two renderings initiated between willPatch and patched", async () => { let parent: any = null; - const steps: string[] = []; class Panel extends Component { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onMounted(() => parent.render()); onWillUnmount(() => parent.render()); } @@ -2506,15 +2490,14 @@ test("two renderings initiated between willPatch and patched", async () => { static components = { Panel }; state = useState({ panel: "Panel1", flag: true }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); parent = this; } } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
Panel1
"); - - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -2525,14 +2508,12 @@ test("two renderings initiated between willPatch and patched", async () => { "Panel:rendered", "Panel:mounted", "Parent:mounted", - ]); - steps.length = 0; + ]).toBeLogged(); parent.state.panel = "Panel2"; await nextTick(); expect(fixture.innerHTML).toBe("
Panel2
"); - - expect(steps).toEqual([ + expect([ "Parent:willRender", "Panel:setup", "Panel:willStart", @@ -2544,33 +2525,29 @@ test("two renderings initiated between willPatch and patched", async () => { "Panel:destroyed", "Panel:mounted", "Parent:patched", - ]); - steps.length = 0; + ]).toBeLogged(); parent.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
"); - - expect(steps).toEqual([ + expect([ "Parent:willRender", "Parent:rendered", "Parent:willPatch", "Panel:willUnmount", "Panel:destroyed", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("parent and child rendered at exact same time", async () => { let child: any = null; - let steps: any[] = []; class Child extends Component { static template = xml``; setup() { child = this; - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -2579,20 +2556,13 @@ test("parent and child rendered at exact same time", async () => { static components = { Child }; state = { value: 0 }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - expect(fixture.innerHTML).toBe("0"); - - parent.state.value = 1; - parent.render(); - child.render(); - await nextTick(); - expect(fixture.innerHTML).toBe("1"); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -2603,6 +2573,14 @@ test("parent and child rendered at exact same time", async () => { "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", @@ -2612,7 +2590,7 @@ test("parent and child rendered at exact same time", async () => { "Child:willPatch", "Child:patched", "Parent:patched", - ]); + ]).toBeLogged(); }); // test.skip("components with shouldUpdate=false", async () => { diff --git a/tests/components/lifecycle.test.ts b/tests/components/lifecycle.test.ts index 4b877991..376c488c 100644 --- a/tests/components/lifecycle.test.ts +++ b/tests/components/lifecycle.test.ts @@ -346,12 +346,10 @@ describe("lifecycle hooks", () => { }); test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => { - let steps: string[] = []; - class Child extends Component { static template = xml``; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -363,7 +361,7 @@ describe("lifecycle hooks", () => { `; static components = { Child }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } state = useState({ n: 0, flag: true }); increment() { @@ -376,13 +374,7 @@ describe("lifecycle hooks", () => { const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
0
"); - parent.increment(); - await nextTick(); - expect(fixture.innerHTML).toBe("
1
"); - parent.toggleSubWidget(); - await nextTick(); - expect(fixture.innerHTML).toBe(""); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -393,6 +385,12 @@ describe("lifecycle hooks", () => { "Child:rendered", "Child:mounted", "Parent:mounted", + ]).toBeLogged(); + + parent.increment(); + await nextTick(); + expect(fixture.innerHTML).toBe("
1
"); + expect([ "Parent:willRender", "Child:willUpdateProps", "Parent:rendered", @@ -402,32 +400,26 @@ describe("lifecycle hooks", () => { "Child:willPatch", "Child:patched", "Parent:patched", + ]).toBeLogged(); + + parent.toggleSubWidget(); + await nextTick(); + expect(fixture.innerHTML).toBe(""); + expect([ "Parent:willRender", "Parent:rendered", "Parent:willPatch", "Child:willUnmount", "Child:destroyed", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("hooks are called in proper order in widget creation/destruction", async () => { - let steps: string[] = []; - class Child extends Component { static template = xml`
`; setup() { - steps.push("c init"); - onWillStart(() => { - steps.push("c willstart"); - }); - onMounted(() => { - steps.push("c mounted"); - }); - onWillUnmount(() => { - steps.push("c willunmount"); - }); + useLogLifecycle(); } } @@ -435,33 +427,32 @@ describe("lifecycle hooks", () => { static template = xml`
`; static components = { Child }; setup() { - steps.push("p init"); - onWillStart(() => { - steps.push("p willstart"); - }); - onMounted(() => { - steps.push("p mounted"); - }); - onWillUnmount(() => { - steps.push("p willunmount"); - }); + useLogLifecycle(); } } const app = new App(Parent); await app.mount(fixture); + expect([ + "Parent:setup", + "Parent:willStart", + "Parent:willRender", + "Child:setup", + "Child:willStart", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + "Child:mounted", + "Parent:mounted", + ]).toBeLogged(); + app.destroy(); - expect(steps).toEqual([ - "p init", - "p willstart", - "c init", - "c willstart", - "c mounted", - "p mounted", - "p willunmount", - "c willunmount", - ]); - Object.freeze(steps); + expect([ + "Parent:willUnmount", + "Child:willUnmount", + "Child:destroyed", + "Parent:destroyed", + ]).toBeLogged(); }); test("willUpdateProps hook is called", async () => { @@ -543,12 +534,10 @@ describe("lifecycle hooks", () => { }); test("lifecycle semantics", async () => { - let steps: string[] = []; - class Child extends Component { static template = xml`
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } class Parent extends Component { @@ -556,14 +545,13 @@ describe("lifecycle hooks", () => { static components = { Child }; state = useState({ a: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const app = new App(Parent); await app.mount(fixture); - - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -574,33 +562,29 @@ describe("lifecycle hooks", () => { "Child:rendered", "Child:mounted", "Parent:mounted", - ]); + ]).toBeLogged(); - steps.splice(0); app.destroy(); - expect(steps).toEqual([ + expect([ "Parent:willUnmount", "Child:willUnmount", "Child:destroyed", "Parent:destroyed", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("lifecycle semantics, part 2", async () => { - let steps: string[] = []; - class GrandChild extends Component { static template = xml`
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } class Child extends Component { static template = xml``; static components = { GrandChild }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -609,26 +593,23 @@ describe("lifecycle hooks", () => { static components = { Child }; state = useState({ hasChild: false }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const app = new App(Parent); const parent = await app.mount(fixture); - - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Parent:rendered", "Parent:mounted", - ]); - - steps.splice(0); + ]).toBeLogged(); parent.state.hasChild = true; await nextTick(); - expect(steps).toEqual([ + expect([ "Parent:willRender", "Child:setup", "Child:willStart", @@ -643,36 +624,31 @@ describe("lifecycle hooks", () => { "GrandChild:mounted", "Child:mounted", "Parent:patched", - ]); - - steps.splice(0); + ]).toBeLogged(); app.destroy(); - expect(steps).toEqual([ + expect([ "Parent:willUnmount", "Child:willUnmount", "GrandChild:willUnmount", "GrandChild:destroyed", "Child:destroyed", "Parent:destroyed", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("lifecycle semantics, part 3", async () => { - let steps: string[] = []; - class GrandChild extends Component { static template = xml`
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } class Child extends Component { static template = xml``; static components = { GrandChild }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -681,41 +657,34 @@ describe("lifecycle hooks", () => { static components = { Child }; state = useState({ hasChild: false }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const app = new App(Parent); const parent = await app.mount(fixture); - - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Parent:rendered", "Parent:mounted", - ]); - - steps.splice(0); + ]).toBeLogged(); parent.state.hasChild = true; - // immediately destroy everything app.destroy(); await nextTick(); - expect(steps).toEqual(["Parent:willUnmount", "Parent:destroyed"]); - Object.freeze(steps); + expect(["Parent:willUnmount", "Parent:destroyed"]).toBeLogged(); }); test("lifecycle semantics, part 4", async () => { let def = makeDeferred(); - let steps: string[] = []; - class GrandChild extends Component { static template = xml`
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillStart(() => def); } } @@ -723,7 +692,7 @@ describe("lifecycle hooks", () => { static template = xml``; static components = { GrandChild }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -732,27 +701,24 @@ describe("lifecycle hooks", () => { static components = { Child }; state = useState({ hasChild: false }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const app = new App(Parent); const parent = await app.mount(fixture); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", "Parent:rendered", "Parent:mounted", - ]); - - steps.splice(0); + ]).toBeLogged(); parent.state.hasChild = true; - await nextTick(); - expect(steps).toEqual([ + expect([ "Parent:willRender", "Child:setup", "Child:willStart", @@ -761,27 +727,22 @@ describe("lifecycle hooks", () => { "GrandChild:setup", "GrandChild:willStart", "Child:rendered", - ]); - - steps.splice(0); + ]).toBeLogged(); app.destroy(); - expect(steps).toEqual([ + expect([ "Parent:willUnmount", "GrandChild:destroyed", "Child:destroyed", "Parent:destroyed", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("lifecycle semantics, part 5", async () => { - let steps: string[] = []; - class Child extends Component { static template = xml`
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -790,13 +751,12 @@ describe("lifecycle hooks", () => { static components = { Child }; state = useState({ hasChild: true }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -807,31 +767,25 @@ describe("lifecycle hooks", () => { "Child:rendered", "Child:mounted", "Parent:mounted", - ]); - - steps.splice(0); + ]).toBeLogged(); parent.state.hasChild = false; - await nextTick(); - expect(steps).toEqual([ + expect([ "Parent:willRender", "Parent:rendered", "Parent:willPatch", "Child:willUnmount", "Child:destroyed", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("lifecycle semantics, part 6", async () => { - let steps: string[] = []; - class Child extends Component { static template = xml`
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -840,13 +794,12 @@ describe("lifecycle hooks", () => { static components = { Child }; state = useState({ value: 1 }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -857,14 +810,11 @@ describe("lifecycle hooks", () => { "Child:rendered", "Child:mounted", "Parent:mounted", - ]); - - steps.splice(0); + ]).toBeLogged(); parent.state.value = 2; - await nextTick(); - expect(steps).toEqual([ + expect([ "Parent:willRender", "Child:willUpdateProps", "Parent:rendered", @@ -874,12 +824,10 @@ describe("lifecycle hooks", () => { "Child:willPatch", "Child:patched", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); test("onWillRender", async () => { - let steps: string[] = []; const def = makeDeferred(); class Child extends Component { @@ -887,13 +835,12 @@ describe("lifecycle hooks", () => { state = useState({ value: 1 }); visibleState = this.state.value; setup() { - useLogLifecycle(steps); + useLogLifecycle(); onWillUpdateProps(() => def); onWillRender(() => (this.visibleState = this.state.value)); } increment() { this.state.value++; - steps.push(`inc:${this.visibleState}`); } } @@ -902,30 +849,13 @@ describe("lifecycle hooks", () => { `; static components = { Child }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - expect(fixture.innerHTML).toBe(""); - - parent.render(); // to block child render - await nextTick(); - - fixture.querySelector("button")!.click(); - - await nextTick(); - fixture.querySelector("button")!.click(); - - await nextTick(); - - expect(fixture.innerHTML).toBe(""); - - def.resolve(); - await nextTick(); - expect(fixture.innerHTML).toBe(""); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -936,19 +866,32 @@ describe("lifecycle hooks", () => { "Child:rendered", "Child:mounted", "Parent:mounted", - "Parent:willRender", - "Child:willUpdateProps", - "Parent:rendered", - "inc:1", - "inc:1", + ]).toBeLogged(); + + parent.render(); // to block child render + await nextTick(); + expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged(); + + fixture.querySelector("button")!.click(); + await nextTick(); + expect([]).toBeLogged(); + + fixture.querySelector("button")!.click(); + await nextTick(); + expect([]).toBeLogged(); + expect(fixture.innerHTML).toBe(""); + + def.resolve(); + await nextTick(); + expect(fixture.innerHTML).toBe(""); + expect([ "Child:willRender", "Child:rendered", "Parent:willPatch", "Child:willPatch", "Child:patched", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); // TODO: rename (remove? seems covered by lifecycle semantics) @@ -982,12 +925,10 @@ describe("lifecycle hooks", () => { // TODO: rename (corresponds to https://github.com/odoo/owl/blob/master/doc/reference/concurrency_model.md#semantics) test("component semantics", async () => { - let steps: string[] = []; - class TestWidget extends Component { name: string = "test"; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } class B extends TestWidget { @@ -1031,7 +972,7 @@ describe("lifecycle hooks", () => { await mount(A, fixture); expect(fixture.innerHTML).toBe(`
A
B
C
D
E
`); - expect(steps).toEqual([ + expect([ "A:setup", "A:willStart", "A:willRender", @@ -1057,13 +998,12 @@ describe("lifecycle hooks", () => { "C:mounted", "B:mounted", "A:mounted", - ]); + ]).toBeLogged(); // update - steps.splice(0); c!.state.flag = false; await nextTick(); - expect(steps).toEqual([ + expect([ "C:willRender", "D:willUpdateProps", "F:setup", @@ -1080,15 +1020,14 @@ describe("lifecycle hooks", () => { "F:mounted", "D:patched", "C:patched", - ]); + ]).toBeLogged(); }); test("mounted hook is called on every mount, not just the first one", async () => { - const steps: string[] = []; class Child extends Component { static template = xml`
child
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -1097,18 +1036,12 @@ describe("lifecycle hooks", () => { static components = { Child }; state = useState({ hasChild: true }); setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - - parent.state.hasChild = false; - await nextTick(); - - parent.state.hasChild = true; - await nextTick(); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -1119,12 +1052,22 @@ describe("lifecycle hooks", () => { "Child:rendered", "Child:mounted", "Parent:mounted", + ]).toBeLogged(); + + parent.state.hasChild = false; + await nextTick(); + expect([ "Parent:willRender", "Parent:rendered", "Parent:willPatch", "Child:willUnmount", "Child:destroyed", "Parent:patched", + ]).toBeLogged(); + + parent.state.hasChild = true; + await nextTick(); + expect([ "Parent:willRender", "Child:setup", "Child:willStart", @@ -1134,7 +1077,6 @@ describe("lifecycle hooks", () => { "Parent:willPatch", "Child:mounted", "Parent:patched", - ]); - Object.freeze(steps); + ]).toBeLogged(); }); }); diff --git a/tests/components/t_component.test.ts b/tests/components/t_component.test.ts index 7ae59a2f..126af0cf 100644 --- a/tests/components/t_component.test.ts +++ b/tests/components/t_component.test.ts @@ -12,12 +12,10 @@ beforeEach(() => { describe("t-component", () => { test("t-component works in simple case", async () => { - let steps: string[] = []; - class Child extends Component { static template = xml`
child
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -25,14 +23,14 @@ describe("t-component", () => { static template = xml``; Child = Child; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
child
"); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -43,23 +41,21 @@ describe("t-component", () => { "Child:rendered", "Child:mounted", "Parent:mounted", - ]); + ]).toBeLogged(); }); test("switching dynamic component", async () => { - let steps: string[] = []; - class ChildA extends Component { static template = xml`
child a
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } class ChildB extends Component { static template = xml`child b`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -67,19 +63,13 @@ describe("t-component", () => { static template = xml``; Child = ChildA; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } const parent = await mount(Parent, fixture); - expect(fixture.innerHTML).toBe("
child a
"); - - parent.Child = ChildB; - parent.render(); - await nextTick(); - expect(fixture.innerHTML).toBe("child b"); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -90,6 +80,13 @@ describe("t-component", () => { "ChildA:rendered", "ChildA:mounted", "Parent:mounted", + ]).toBeLogged(); + + parent.Child = ChildB; + parent.render(); + await nextTick(); + expect(fixture.innerHTML).toBe("child b"); + expect([ "Parent:willRender", "ChildB:setup", "ChildB:willStart", @@ -101,7 +98,7 @@ describe("t-component", () => { "ChildA:destroyed", "ChildB:mounted", "Parent:patched", - ]); + ]).toBeLogged(); }); test("can switch between dynamic components without the need for a t-key", async () => { diff --git a/tests/components/t_foreach.test.ts b/tests/components/t_foreach.test.ts index 7f3d010e..8ace03d0 100644 --- a/tests/components/t_foreach.test.ts +++ b/tests/components/t_foreach.test.ts @@ -49,12 +49,10 @@ describe("list of components", () => { }); test("components in a node in a t-foreach ", async () => { - const steps: string[] = []; - class Child extends Component { static template = xml`
`; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } } @@ -72,7 +70,7 @@ describe("list of components", () => { static components = { Child }; setup() { - useLogLifecycle(steps); + useLogLifecycle(); } get items() { @@ -84,7 +82,7 @@ describe("list of components", () => { expect(fixture.innerHTML).toBe( "
  • 1
  • 2
" ); - expect(steps).toEqual([ + expect([ "Parent:setup", "Parent:willStart", "Parent:willRender", @@ -100,7 +98,7 @@ describe("list of components", () => { "Child:mounted", "Child:mounted", "Parent:mounted", - ]); + ]).toBeLogged(); }); test("reconciliation alg works for t-foreach in t-foreach", async () => { diff --git a/tests/helpers.ts b/tests/helpers.ts index 70d811f5..29555c08 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -161,53 +161,58 @@ export function snapshotEverything() { }); } -export function useLogLifecycle(steps: string[]) { +const steps: string[] = []; + +export function logStep(step: string) { + steps.push(step); +} +export function useLogLifecycle() { const component = useComponent(); const name = component.constructor.name; - steps.push(`${name}:setup`); + logStep(`${name}:setup`); expect(name + ": " + status(component)).toBe(name + ": " + "new"); onWillStart(() => { expect(name + ": " + status(component)).toBe(name + ": " + "new"); - steps.push(`${name}:willStart`); + logStep(`${name}:willStart`); }); onMounted(() => { expect(name + ": " + status(component)).toBe(name + ": " + "mounted"); - steps.push(`${name}:mounted`); + logStep(`${name}:mounted`); }); onWillUpdateProps(() => { expect(name + ": " + status(component)).toBe(name + ": " + "mounted"); - steps.push(`${name}:willUpdateProps`); + logStep(`${name}:willUpdateProps`); }); onWillRender(() => { - steps.push(`${name}:willRender`); + logStep(`${name}:willRender`); }); onRendered(() => { - steps.push(`${name}:rendered`); + logStep(`${name}:rendered`); }); onWillPatch(() => { expect(name + ": " + status(component)).toBe(name + ": " + "mounted"); - steps.push(`${name}:willPatch`); + logStep(`${name}:willPatch`); }); onPatched(() => { expect(name + ": " + status(component)).toBe(name + ": " + "mounted"); - steps.push(`${name}:patched`); + logStep(`${name}:patched`); }); onWillUnmount(() => { expect(name + ": " + status(component)).toBe(name + ": " + "mounted"); - steps.push(`${name}:willUnmount`); + logStep(`${name}:willUnmount`); }); onDestroyed(() => { expect(name + ": " + status(component)).toBe(name + ": " + "destroyed"); - steps.push(`${name}:destroyed`); + logStep(`${name}:destroyed`); }); } @@ -226,3 +231,55 @@ export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, v input.dispatchEvent(new Event("change")); return nextTick(); } + +import { diff } from "jest-diff"; + +afterEach(() => { + if (steps.length) { + steps.splice(0); + throw new Error("Remaining steps! Should be checked by a .toBeLogged() assertion!"); + } +}); + +expect.extend({ + toBeLogged(expected) { + const options = { + comment: "steps equality", + isNot: this.isNot, + promise: this.promise, + }; + + const currentSteps = steps.splice(0); + const pass = this.equals(currentSteps, expected); + + const message = pass + ? () => + this.utils.matcherHint("toEqual", undefined, undefined, options) + + "\n\n" + + `Expected: not ${this.utils.printExpected(expected)}\n` + + `Received: ${this.utils.printReceived(currentSteps)}` + : () => { + const diffString = diff(expected, currentSteps, { + expand: this.expand, + }); + return ( + this.utils.matcherHint("toBe", undefined, undefined, options) + + "\n\n" + + (diffString && diffString.includes("- Expect") + ? `Difference:\n\n${diffString}` + : `Expected: ${this.utils.printExpected(expected)}\n` + + `Received: ${this.utils.printReceived(currentSteps)}`) + ); + }; + + return { actual: currentSteps, message, pass }; + }, +}); + +declare global { + namespace jest { + interface Matchers { + toBeLogged(): R; + } + } +} diff --git a/tests/reactivity.test.ts b/tests/reactivity.test.ts index 2836fe04..f822762b 100644 --- a/tests/reactivity.test.ts +++ b/tests/reactivity.test.ts @@ -6,7 +6,7 @@ import { makeTestFixture, nextMicroTick, nextTick, - snapshotEverything, + snapshotEverything } from "./helpers"; function createReactive(value: any, observer: any = () => {}) {