diff --git a/src/component/component.ts b/src/component/component.ts index 93fa3ec7..942f2dea 100644 --- a/src/component/component.ts +++ b/src/component/component.ts @@ -373,6 +373,9 @@ export class Component { const fiber = new Fiber(null, this, force, null); Promise.resolve().then(() => { if (__owl__.isMounted || !isMounted) { + if (fiber.isCompleted) { + return; + } // we are mounted (__owl__.isMounted), or if we are currently being // mounted (!isMounted), so we call __render this.__render(fiber); diff --git a/tests/component/async.test.ts b/tests/component/async.test.ts index c89113d3..fb1dd593 100644 --- a/tests/component/async.test.ts +++ b/tests/component/async.test.ts @@ -1152,6 +1152,220 @@ describe("async rendering", () => { expect(fixture.innerHTML).toBe("
01
"); }); + test("concurrent renderings scenario 14", async () => { + let b: B | undefined = undefined; + let c: C | undefined = undefined; + class C extends Component { + static template = xml` +

+ + + +

`; + + state = useState({ fromC: 3 }); + constructor(parent, props) { + super(parent, props); + c = this; + } + } + class B extends Component { + static template = xml`

`; + static components = { C }; + constructor(parent, props) { + super(parent, props); + b = this; + } + state = useState({ fromB: 2 }); + } + class A extends Component { + static template = xml`

`; + static components = { B }; + state = useState({ fromA: 1 }); + } + const a = new A(); + await a.mount(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

" + ); + }); + + test("concurrent renderings scenario 15", async () => { + let b: B | undefined = undefined; + let c: C | undefined = undefined; + class C extends Component { + static template = xml` +

+ + + +

`; + + state = useState({ fromC: 3 }); + constructor(parent, props) { + super(parent, props); + c = this; + } + } + class B extends Component { + static template = xml`

`; + static components = { C }; + constructor(parent, props) { + super(parent, props); + b = this; + } + state = useState({ fromB: 2 }); + } + class A extends Component { + static template = xml`

`; + static components = { B }; + state = useState({ fromA: 1 }); + } + const a = new A(); + await a.mount(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; + + // simulate a flush (nothing should have changed as no fiber should have its + // counter to 0) + Component.scheduler.flush(); + expect(fixture.innerHTML).toBe( + "

123

" + ); + + // wait a bit and simulate another flush (we expect nothing to change as well) + await nextMicroTick(); + Component.scheduler.flush(); + expect(fixture.innerHTML).toBe( + "

123

" + ); + + await nextTick(); + expect(fixture.innerHTML).toBe( + "

111213

" + ); + }); + + test("concurrent renderings scenario 16", async () => { + expect.assertions(4); + let b: B | undefined = undefined; + let c: C | undefined = undefined; + class D extends Component { + static template = xml`
    DDD
`; + async willStart() { + await nextTick(); + await nextTick(); + } + } + class C extends Component { + static template = xml` +

+ + + + +

`; + static components = { D }; + state = { fromC: 3 }; // not reactive + constructor(parent, props) { + super(parent, props); + c = this; + } + } + class B extends Component { + static template = xml`

`; + static components = { C }; + constructor(parent, props) { + super(parent, props); + b = this; + } + state = useState({ fromB: 2 }); + } + class A extends Component { + static template = xml`

`; + static components = { B }; + state = useState({ fromA: 1 }); + } + const a = new A(); + await a.mount(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; + const prom = c!.render().then(() => { + expect(fixture.innerHTML).toBe( + "

111213

    DDD

" + ); + }); + // trigger a re-rendering from B, which will remap its new fiber as well + b!.state.fromB += 10; + + await nextTick(); + // at this point, C rendering is still pending, and nothing should have been + // updated yet. + expect(fixture.innerHTML).toBe( + "

123

" + ); + await prom; + }); + test("change state and call manually render: no unnecessary rendering", async () => { class Widget extends Component { static template = xml`
`;