From e5940b4b6b325aa3da3c7d59672f9ec2caa92b80 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 5 Nov 2019 14:23:06 +0100 Subject: [PATCH] [FIX] component: concurrent calls to mount and render Closes #450 --- src/component/component.ts | 82 ++++-------- src/component/directive.ts | 2 +- src/component/fiber.ts | 78 +++++++++-- src/component/scheduler.ts | 33 +++-- src/hooks.ts | 12 +- src/qweb/base_directives.ts | 8 +- tests/__snapshots__/animations.test.ts.snap | 4 +- .../__snapshots__/component.test.ts.snap | 56 ++++---- .../props_validation.test.ts.snap | 2 +- tests/component/component.test.ts | 125 +++++++++++++++++- .../route_component.test.ts.snap | 2 +- 11 files changed, 292 insertions(+), 112 deletions(-) diff --git a/src/component/component.ts b/src/component/component.ts index c8b350bd..df014627 100644 --- a/src/component/component.ts +++ b/src/component/component.ts @@ -1,6 +1,6 @@ import { Observer } from "../core/observer"; import { CompiledTemplate, QWeb } from "../qweb/index"; -import { h, patch, VNode } from "../vdom/index"; +import { patch, VNode } from "../vdom/index"; import "./directive"; import { Fiber } from "./fiber"; import "./props_validation"; @@ -282,32 +282,19 @@ export class Component { return Promise.resolve(); } if (!(target instanceof HTMLElement)) { - let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`; + let message = `Component '${ + this.constructor.name + }' cannot be mounted: the target is not a valid DOM node.`; message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`; throw new Error(message); } - return new Promise((resolve, reject) => { - const fiber = new Fiber(null, this, undefined, undefined, false); - scheduler.addFiber(fiber, err => { - if (err) { - reject(err); - return; - } - if (!__owl__.isDestroyed) { - this.__patch(fiber.vnode); - target.appendChild(this.el!); - if (document.body.contains(target)) { - this.__callMounted(); - } - } - resolve(); - }); - if (!__owl__.vnode) { - this.__prepareAndRender(fiber); - } else { - this.__render(fiber); - } - }); + const fiber = new Fiber(null, this, undefined, undefined, false, target); + if (!__owl__.vnode) { + this.__prepareAndRender(fiber); + } else { + this.__render(fiber); + } + return scheduler.addFiber(fiber); } /** @@ -332,26 +319,18 @@ export class Component { */ async render(force: boolean = false): Promise { const __owl__ = this.__owl__; - if ( - (!__owl__.isMounted && !__owl__.currentFiber) || - (__owl__.currentFiber && !__owl__.currentFiber.isRendered) - ) { + if (!__owl__.isMounted && !__owl__.currentFiber) { + // if we get here, this means that the component was either never mounted, + // or was unmounted and some state change triggered a render. Either way, + // we do not want to actually render anything in this case. return; } - return new Promise((resolve, reject) => { - const fiber = new Fiber(null, this, undefined, undefined, force); - scheduler.addFiber(fiber.root, err => { - if (err) { - reject(err); - return; - } - if (__owl__.isMounted && fiber === fiber.root) { - fiber.patchComponents(); - } - resolve(); - }); - this.__render(fiber); - }); + if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) { + return scheduler.addFiber(__owl__.currentFiber.root); + } + const fiber = new Fiber(null, this, undefined, undefined, force, null); + this.__render(fiber); + return scheduler.addFiber(fiber); } /** @@ -437,7 +416,7 @@ export class Component { __owl__.isDestroyed = true; delete __owl__.vnode; if (__owl__.currentFiber) { - __owl__.currentFiber.isCancelled = true; + __owl__.currentFiber.isCompleted = true; } } @@ -492,7 +471,7 @@ export class Component { const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps); if (shouldUpdate) { const __owl__ = this.__owl__; - const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force); + const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null); if (!parentFiber.child) { parentFiber.child = fiber; } else { @@ -510,7 +489,7 @@ export class Component { this.willUpdateProps(nextProps), __owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps) ]); - if (fiber.isCancelled) { + if (fiber.isCompleted) { return; } this.props = nextProps; @@ -535,7 +514,7 @@ export class Component { * parent template. */ __prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) { - const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force); + const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null); fiber.shouldPatch = false; if (!parentFiber.child) { parentFiber.child = fiber; @@ -569,13 +548,12 @@ export class Component { await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]); } catch (e) { fiber.handleError(e); - fiber.vnode = h("div"); // -> we render this div at the end return Promise.resolve(); } if (this.__owl__.isDestroyed) { return Promise.resolve(); } - if (!fiber.isCancelled) { + if (!fiber.isCompleted) { this.__render(fiber); } } @@ -585,17 +563,14 @@ export class Component { if (__owl__.observer) { __owl__.observer.allowMutations = false; } - let vnode; try { - vnode = __owl__.renderFn!(this, { + fiber.vnode = __owl__.renderFn!(this, { handlers: __owl__.boundHandlers, fiber: fiber }); } catch (e) { - vnode = __owl__.vnode || h("div"); fiber.handleError(e); } - fiber.vnode = vnode; if (__owl__.observer) { __owl__.observer.allowMutations = true; } @@ -604,7 +579,8 @@ export class Component { // template (so, something like ) to the actual // root vnode if (__owl__.classObj) { - vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj); + const data = fiber.vnode!.data!; + data.class = Object.assign(data.class || {}, __owl__.classObj); } fiber.root.counter--; fiber.isRendered = true; diff --git a/src/component/directive.ts b/src/component/directive.ts index 130e37d7..4f428cd9 100644 --- a/src/component/directive.ts +++ b/src/component/directive.ts @@ -455,7 +455,7 @@ QWeb.addDirective({ ); ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`); ctx.addLine( - `def${defID}.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});` + `def${defID}.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});` ); if (registerCode) { ctx.addLine(registerCode); diff --git a/src/component/fiber.ts b/src/component/fiber.ts index fab98f70..5a741eaf 100644 --- a/src/component/fiber.ts +++ b/src/component/fiber.ts @@ -1,4 +1,4 @@ -import { VNode } from "../vdom/index"; +import { h, VNode } from "../vdom/index"; import { Component } from "./component"; import { scheduler } from "./scheduler"; @@ -20,9 +20,10 @@ export class Fiber { // method potentially implemented by a component. It is usually set to false. force: boolean; - // isCancelled means that the rendering corresponding to this fiber and its - // children is cancelled. No extra work should be done. - isCancelled: boolean = false; + // isCompleted means that the rendering corresponding to this fiber's work is + // done, either because the component has been mounted or patched, or because + // fiber has been cancelled. + isCompleted: boolean = false; // the fibers corresponding to component updates (updateProps) need to call // the willPatch and patched hooks from the corresponding component. However, @@ -42,6 +43,8 @@ export class Fiber { // scheduler. counter: number = 0; + target: HTMLElement | null; + scope: any; vars: any; @@ -55,18 +58,25 @@ export class Fiber { error?: Error; - constructor(parent: Fiber | null, component: Component, scope, vars, force) { + constructor(parent: Fiber | null, component: Component, scope, vars, force, target) { this.force = force; this.scope = scope; this.vars = vars; this.component = component; + this.target = target; this.root = parent ? parent.root : this; this.parent = parent; let oldFiber = component.__owl__.currentFiber; - if (oldFiber && !oldFiber.isCancelled) { - this._remapFiber(oldFiber); + if (oldFiber && !oldFiber.isCompleted) { + if (oldFiber.root === oldFiber && !parent) { + // both oldFiber and this fiber are root fibers + this._reuseFiber(oldFiber); + return oldFiber; + } else { + this._remapFiber(oldFiber); + } } this.root.counter++; @@ -74,6 +84,25 @@ export class Fiber { component.__owl__.currentFiber = this; } + /** + * When the oldFiber is not completed yet, and both oldFiber and this fiber + * are root fibers, we want to reuse the oldFiber instead of creating a new + * one. Doing so will guarantee that the initiator(s) of those renderings will + * be notified (the promise will resolve) when the last rendering will be done. + * + * This function thus assumes that oldFiber is a root fiber. + */ + _reuseFiber(oldFiber: Fiber) { + oldFiber.cancel(); // cancel children fibers + oldFiber.isCompleted = false; // keep the root fiber alive + if (oldFiber.child) { + // remove relation to children + oldFiber.child.parent = null; + oldFiber.child = null; + } + oldFiber.counter = 1; // re-initialize counter + } + /** * In some cases, a rendering initiated at some component can detect that it * should be part of a larger rendering initiated somewhere up the component @@ -83,7 +112,7 @@ export class Fiber { _remapFiber(oldFiber: Fiber) { oldFiber.cancel(); if (oldFiber === oldFiber.root) { - oldFiber.root.counter++; + oldFiber.counter++; } if (oldFiber.parent && !this.parent) { // re-map links @@ -132,7 +161,28 @@ export class Fiber { } /** - * Apply the given patch queue from a fiber. + * Successfully complete the work of the fiber: call the mount or patch hooks + * and patch the DOM. This function is called once the fiber and its children + * are ready, and the scheduler decides to process it. + */ + complete() { + const component = this.component; + if (this.target) { + component.__patch(this.vnode); + this.target.appendChild(component.el!); + if (document.body.contains(this.target)) { + component.__callMounted(); + } + } else { + if (component.__owl__.isMounted && this === this.root) { + this.patchComponents(); + } + } + this.isCompleted = true; + } + + /** + * Compute and apply the patch queue of the fiber. * 1) Call 'willPatch' on the component of each patch * 2) Call '__patch' on the component of each patch * 3) Call 'patched' on the component of each patch, in reverse order @@ -142,8 +192,8 @@ export class Fiber { const doWork: (Fiber) => Fiber | null = function(f) { if (f.shouldPatch) { patchQueue.push(f); + return f.child; } - return f.child; }; this._walk(doWork); let component: Component = this.component; @@ -186,7 +236,7 @@ export class Fiber { if (!f.isRendered) { f.root.counter--; } - f.isCancelled = true; + f.isCompleted = true; return f.child; }); } @@ -200,10 +250,12 @@ export class Fiber { * being in a corrupted state. */ handleError(error: Error) { - let canCatch = false; let component = this.component; - let qweb = component.env.qweb; + this.vnode = component.__owl__.vnode || h("div"); + + const qweb = component.env.qweb; let root = component; + let canCatch = false; while (component && !(canCatch = !!component.catchError)) { root = component; component = component.__owl__.parent!; diff --git a/src/component/scheduler.ts b/src/component/scheduler.ts index 6b496852..917f1bc5 100644 --- a/src/component/scheduler.ts +++ b/src/component/scheduler.ts @@ -25,12 +25,25 @@ export class Scheduler { this.requestAnimationFrame = requestAnimationFrame; } - addFiber(fiber, callback) { - this.tasks.push({ fiber, callback }); - if (this.isRunning) { - return; - } - this.scheduleTasks(); + addFiber(fiber): Promise { + return new Promise((resolve, reject) => { + if (fiber.error) { + return reject(fiber.error); + } + this.tasks.push({ + fiber, + callback: () => { + if (fiber.error) { + reject(fiber.error); + return; + } + resolve(); + } + }); + if (!this.isRunning) { + this.scheduleTasks(); + } + }); } /** @@ -41,11 +54,15 @@ export class Scheduler { let tasks = this.tasks; this.tasks = []; tasks = tasks.filter(task => { - if (task.fiber.isCancelled) { + if (task.fiber.isCompleted) { + task.callback(); return false; } if (task.fiber.counter === 0) { - task.callback(task.fiber.error); + if (!task.fiber.error) { + task.fiber.complete(); + } + task.callback(); return false; } return true; diff --git a/src/hooks.ts b/src/hooks.ts index 99188b89..c572742e 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -24,9 +24,19 @@ import { Observer } from "./core/observer"; export function useState(state: T): T { const component: Component = Component.current!; const __owl__ = component.__owl__; + const renderFn = __owl__.renderFn; + let lastRenderRevNumber; + __owl__.renderFn = function(comp, params) { + lastRenderRevNumber = __owl__.observer!.rev; + return renderFn(comp, params); + }; if (!__owl__.observer) { __owl__.observer = new Observer(); - __owl__.observer.notifyCB = component.render.bind(component); + __owl__.observer.notifyCB = () => { + if (lastRenderRevNumber < __owl__.observer!.rev) { + component.render(); + } + }; } return __owl__.observer.observe(state); } diff --git a/src/qweb/base_directives.ts b/src/qweb/base_directives.ts index f791a116..c63e91cc 100644 --- a/src/qweb/base_directives.ts +++ b/src/qweb/base_directives.ts @@ -236,7 +236,9 @@ QWeb.addDirective({ varCode = `{${content}}`; } ctx.addLine( - `this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));` + `this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ + ctx.parentNode + }, fiber: {vars: ${varCode}, scope}}));` ); return true; } @@ -305,7 +307,9 @@ QWeb.addDirective({ !node.children[0].hasAttribute("t-key"); if (shouldWarn) { console.warn( - `Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')` + `Directive t-foreach should always be used with a t-key! (in template: '${ + ctx.templateName + }')` ); } diff --git a/tests/__snapshots__/animations.test.ts.snap b/tests/__snapshots__/animations.test.ts.snap index a60216e8..55bb540a 100644 --- a/tests/__snapshots__/animations.test.ts.snap +++ b/tests/__snapshots__/animations.test.ts.snap @@ -35,7 +35,7 @@ exports[`animations t-transition combined with component 1`] = ` }; utils.transitionRemove(vn, 'chimay', finalize);}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -80,7 +80,7 @@ exports[`animations t-transition combined with t-component and t-if 1`] = ` }; utils.transitionRemove(vn, 'chimay', finalize);}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } diff --git a/tests/component/__snapshots__/component.test.ts.snap b/tests/component/__snapshots__/component.test.ts.snap index ffe6d669..2bb80a33 100644 --- a/tests/component/__snapshots__/component.test.ts.snap +++ b/tests/component/__snapshots__/component.test.ts.snap @@ -61,7 +61,7 @@ exports[`basic widget properties reconciliation alg works for t-foreach in t-for let def8 = w9.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId9, hook: {insert(vn) { let nvn=w9.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w9.destroy();}}}); const fiber = w9.__owl__.currentFiber; - def8.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def8.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w9.__owl__.pvnode = pvnode; } @@ -105,7 +105,7 @@ exports[`class and style attributes with t-component dynamic t-att-style is prop let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.style = _5;}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.style = _5;}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -150,7 +150,7 @@ exports[`class and style attributes with t-component t-att-class is properly add let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;context.__owl__.refs[ref5] = w3;},remove() {},destroy(vn) {w3.destroy();delete context.__owl__.refs[ref5];}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -210,7 +210,7 @@ exports[`class and style attributes with t-component t-att-class is properly add let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;context.__owl__.refs[ref5] = w3;},remove() {},destroy(vn) {w3.destroy();delete context.__owl__.refs[ref5];}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -282,7 +282,7 @@ exports[`composition sub components with some state rendered in a loop 1`] = ` let def6 = w7.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId7, hook: {insert(vn) { let nvn=w7.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}}); const fiber = w7.__owl__.currentFiber; - def6.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def6.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w7.__owl__.pvnode = pvnode; } @@ -324,7 +324,7 @@ exports[`composition t-component with dynamic value 1`] = ` let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -365,7 +365,7 @@ exports[`composition t-component with dynamic value 2 1`] = ` let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -406,7 +406,7 @@ exports[`dynamic t-props basic use 1`] = ` let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -451,7 +451,7 @@ exports[`other directives with t-component t-on with getter as handler 1`] = ` let def3 = w4.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId4, hook: {insert(vn) { let nvn=w4.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}}); const fiber = w4.__owl__.currentFiber; - def3.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['handler'];if (fn) { fn.call(owner, e); } else { owner.handler; }});}};}); + def3.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['handler'];if (fn) { fn.call(owner, e); } else { owner.handler; }});}};}); c1.push(pvnode); w4.__owl__.pvnode = pvnode; } @@ -492,7 +492,7 @@ exports[`other directives with t-component t-on with handler bound to argument 1 let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, 3, e); } else { owner.onEv; }});}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, 3, e); } else { owner.onEv; }});}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -533,7 +533,7 @@ exports[`other directives with t-component t-on with handler bound to empty obje let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, {}, e); } else { owner.onEv; }});}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, {}, e); } else { owner.onEv; }});}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -574,7 +574,7 @@ exports[`other directives with t-component t-on with handler bound to empty obje let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, {}, e); } else { owner.onEv; }});}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, {}, e); } else { owner.onEv; }});}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -615,7 +615,7 @@ exports[`other directives with t-component t-on with handler bound to object 1`] let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, {val:3}, e); } else { owner.onEv; }});}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, {val:3}, e); } else { owner.onEv; }});}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -660,7 +660,7 @@ exports[`other directives with t-component t-on with inline statement 1`] = ` let def3 = w4.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId4, hook: {insert(vn) { let nvn=w4.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}}); const fiber = w4.__owl__.currentFiber; - def3.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['state.counter++'];if (fn) { fn.call(owner, e); } else { owner.state.counter++; }});}};}); + def3.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['state.counter++'];if (fn) { fn.call(owner, e); } else { owner.state.counter++; }});}};}); c1.push(pvnode); w4.__owl__.pvnode = pvnode; } @@ -701,7 +701,7 @@ exports[`other directives with t-component t-on with no handler (only modifiers) let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, e); } else { owner.onEv; }});}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, e); } else { owner.onEv; }});}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -742,7 +742,7 @@ exports[`other directives with t-component t-on with prevent and self modifiers let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {e.preventDefault();if (e.target !== vn.elm) {return}const fn = owner['onEv'];if (fn) { fn.call(owner, e); } else { owner.onEv; }});}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {e.preventDefault();if (e.target !== vn.elm) {return}const fn = owner['onEv'];if (fn) { fn.call(owner, e); } else { owner.onEv; }});}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -783,7 +783,7 @@ exports[`other directives with t-component t-on with self and prevent modifiers let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (e.target !== vn.elm) {return}e.preventDefault();const fn = owner['onEv'];if (fn) { fn.call(owner, e); } else { owner.onEv; }});}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (e.target !== vn.elm) {return}e.preventDefault();const fn = owner['onEv'];if (fn) { fn.call(owner, e); } else { owner.onEv; }});}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -824,7 +824,7 @@ exports[`other directives with t-component t-on with self modifier 1`] = ` let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {const fn = owner['onEv1'];if (fn) { fn.call(owner, e); } else { owner.onEv1; }});vn.elm.addEventListener('ev-2', function (e) {if (e.target !== vn.elm) {return}const fn = owner['onEv2'];if (fn) { fn.call(owner, e); } else { owner.onEv2; }});}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {const fn = owner['onEv1'];if (fn) { fn.call(owner, e); } else { owner.onEv1; }});vn.elm.addEventListener('ev-2', function (e) {if (e.target !== vn.elm) {return}const fn = owner['onEv2'];if (fn) { fn.call(owner, e); } else { owner.onEv2; }});}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -865,7 +865,7 @@ exports[`other directives with t-component t-on with stop and/or prevent modifie let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {e.stopPropagation();const fn = owner['onEv1'];if (fn) { fn.call(owner, e); } else { owner.onEv1; }});vn.elm.addEventListener('ev-2', function (e) {e.preventDefault();const fn = owner['onEv2'];if (fn) { fn.call(owner, e); } else { owner.onEv2; }});vn.elm.addEventListener('ev-3', function (e) {e.stopPropagation();e.preventDefault();const fn = owner['onEv3'];if (fn) { fn.call(owner, e); } else { owner.onEv3; }});}};}); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {e.stopPropagation();const fn = owner['onEv1'];if (fn) { fn.call(owner, e); } else { owner.onEv1; }});vn.elm.addEventListener('ev-2', function (e) {e.preventDefault();const fn = owner['onEv2'];if (fn) { fn.call(owner, e); } else { owner.onEv2; }});vn.elm.addEventListener('ev-3', function (e) {e.stopPropagation();e.preventDefault();const fn = owner['onEv3'];if (fn) { fn.call(owner, e); } else { owner.onEv3; }});}};}); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -925,7 +925,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = ` let def3 = w4.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId4, hook: {insert(vn) { let nvn=w4.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}}); const fiber = w4.__owl__.currentFiber; - def3.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def3.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w4.__owl__.pvnode = pvnode; } @@ -983,7 +983,7 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument let def6 = w7.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId7, hook: {insert(vn) { let nvn=w7.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}}); const fiber = w7.__owl__.currentFiber; - def6.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, arg9, e); } else { owner.onEv; }});}};}); + def6.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {const fn = owner['onEv'];if (fn) { fn.call(owner, arg9, e); } else { owner.onEv; }});}};}); c1.push(pvnode); w7.__owl__.pvnode = pvnode; } @@ -1231,7 +1231,7 @@ exports[`t-slot directive can define and call slots 1`] = ` let def2 = w3.__prepare(extra.fiber, {}, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } @@ -1477,7 +1477,7 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = ` let def8 = w9.__prepare(extra.fiber, Object.assign({}, scope), undefined, sibling); let pvnode = h('dummy', {key: templateId9, hook: {insert(vn) { let nvn=w9.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w9.destroy();}}}); const fiber = w9.__owl__.currentFiber; - def8.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def8.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c7.push(pvnode); w9.__owl__.pvnode = pvnode; } @@ -1581,7 +1581,7 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = ` let def9 = w10.__prepare(extra.fiber, Object.assign({}, scope), {_8}, sibling); let pvnode = h('dummy', {key: templateId10, hook: {insert(vn) { let nvn=w10.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w10.destroy();}}}); const fiber = w10.__owl__.currentFiber; - def9.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def9.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c7.push(pvnode); w10.__owl__.pvnode = pvnode; } @@ -1639,7 +1639,7 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = ` let def3 = w4.__prepare(extra.fiber, {}, {_2}, sibling); let pvnode = h('dummy', {key: templateId4, hook: {insert(vn) { let nvn=w4.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}}); const fiber = w4.__owl__.currentFiber; - def3.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def3.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w4.__owl__.pvnode = pvnode; } @@ -1714,7 +1714,7 @@ exports[`top level sub widgets basic use 1`] = ` let def1 = w2.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId2, hook: {insert(vn) { let nvn=w2.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w2.destroy();}}}); const fiber = w2.__owl__.currentFiber; - def1.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def1.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); utils.defineProxy(vn4, pvnode); w2.__owl__.pvnode = pvnode; } @@ -1757,7 +1757,7 @@ exports[`top level sub widgets can select a sub widget 1`] = ` let def1 = w2.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId2, hook: {insert(vn) { let nvn=w2.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w2.destroy();}}}); const fiber = w2.__owl__.currentFiber; - def1.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def1.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); utils.defineProxy(vn4, pvnode); w2.__owl__.pvnode = pvnode; } @@ -1787,7 +1787,7 @@ exports[`top level sub widgets can select a sub widget 1`] = ` let def5 = w6.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId6, hook: {insert(vn) { let nvn=w6.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w6.destroy();}}}); const fiber = w6.__owl__.currentFiber; - def5.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def5.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); utils.defineProxy(vn8, pvnode); w6.__owl__.pvnode = pvnode; } diff --git a/tests/component/__snapshots__/props_validation.test.ts.snap b/tests/component/__snapshots__/props_validation.test.ts.snap index 28e6f749..5d3b9327 100644 --- a/tests/component/__snapshots__/props_validation.test.ts.snap +++ b/tests/component/__snapshots__/props_validation.test.ts.snap @@ -32,7 +32,7 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] = let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); c1.push(pvnode); w3.__owl__.pvnode = pvnode; } diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index ecbb32b4..36d005ea 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -3348,7 +3348,6 @@ describe("async rendering", () => { const componentA = new ComponentA(); await componentA.mount(fixture); expect(fixture.innerHTML).toBe("

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

"); @@ -3363,6 +3362,64 @@ describe("async rendering", () => { expect(fixture.innerHTML).toBe("

2

"); expect(ComponentC.prototype.__render).toHaveBeenCalledTimes(1); }); + + test("concurrent renderings scenario 11", async () => { + // This scenario is the following: we have a component being updated (by props), + // and then rendered (render method), but before the willUpdateProps resolves. + // We check that in that case, the return value of the render method is a promise + // that is resolved when the component is completely rendered (so, properly + // remapped to the promise of the ambient rendering) + const def = makeDeferred(); + let child; + class Child extends Component { + static template = xml`|`; + val = 3; + willUpdateProps() { + child = this; + return def; + } + } + + class Parent extends Component { + static template = xml`
`; + static components = { Child }; + state = useState({ valA: 1 }); + } + const parent = new Parent(); + await parent.mount(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
"); + }); + + test("change state and call manually render: no unnecessary rendering", async () => { + // when the state is changed, the component isn't notified directly (we wait + // for a microtask tick before calling 'render'), so it may happen that + // another rendering is done meanwhile, already using the new value of the + // state + class Widget extends Component { + static template = xml`
`; + state = useState({ val: 1 }); + } + Widget.prototype.__render = jest.fn(Widget.prototype.__render); + + const widget = new Widget(); + await widget.mount(fixture); + expect(fixture.innerHTML).toBe("
1
"); + expect(Widget.prototype.__render).toHaveBeenCalledTimes(1); + + widget.state.val = 2; + await widget.render(); + expect(fixture.innerHTML).toBe("
2
"); + expect(Widget.prototype.__render).toHaveBeenCalledTimes(2); + }); }); describe("widget and observable state", () => { @@ -4890,7 +4947,7 @@ describe("unmounting and remounting", () => { expect(fixture.innerHTML).toBe("
3
"); }); - test("sub component is still active after being unmounted and remounted", async () => { + test("sub component is still active after being unmounted and remounted", async () => { class Child extends Component { static template = xml`

@@ -4920,6 +4977,70 @@ describe("unmounting and remounting", () => { await nextTick(); expect(fixture.innerHTML).toBe("

3

"); }); + + test("change state just before mounting component", async () => { + const steps: number[] = []; + class TestWidget extends Component { + static template = xml` +
+ `; + state = useState({ val: 1 }); + __render(f) { + steps.push(this.state.val); + return super.__render(f); + } + } + TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render); + + const widget = new TestWidget(); + widget.state.val = 2; + await widget.mount(fixture); + expect(fixture.innerHTML).toBe("
2
"); + expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1); + + // unmount and re-mount, as in this case, willStart won't be called, so it's + // slightly different + widget.unmount(); + widget.state.val = 3; + await widget.mount(fixture); + expect(fixture.innerHTML).toBe("
3
"); + expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(2); + expect(steps).toEqual([2, 3]); + }); + + test("change state while mounting component", async () => { + const steps: number[] = []; + class TestWidget extends Component { + static template = xml` +
+ `; + state = useState({ val: 1 }); + __render(f) { + steps.push(this.state.val); + return super.__render(f); + } + } + TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render); + TestWidget.prototype.__patch = jest.fn(TestWidget.prototype.__patch); + + const widget = new TestWidget(); + let prom = widget.mount(fixture); + widget.state.val = 2; + await prom; + expect(fixture.innerHTML).toBe("
2
"); + expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1); + + // unmount and re-mount, as in this case, willStart won't be called, so it's + // slightly different + widget.unmount(); + prom = widget.mount(fixture); + widget.state.val = 3; + await prom; + expect(fixture.innerHTML).toBe("
3
"); + expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(3); + expect(TestWidget.prototype.__patch).toHaveBeenCalledTimes(2); + expect(steps).toEqual([2, 2, 3]); + }); }); describe("dynamic root nodes", () => { diff --git a/tests/router/__snapshots__/route_component.test.ts.snap b/tests/router/__snapshots__/route_component.test.ts.snap index 3e6ac6e3..55d5e0fc 100644 --- a/tests/router/__snapshots__/route_component.test.ts.snap +++ b/tests/router/__snapshots__/route_component.test.ts.snap @@ -35,7 +35,7 @@ exports[`RouteComponent can render simple cases 1`] = ` let def2 = w3.__prepare(extra.fiber, undefined, undefined, sibling); let pvnode = h('dummy', {key: templateId3, hook: {insert(vn) { let nvn=w3.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}}); const fiber = w3.__owl__.currentFiber; - def2.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + def2.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); utils.defineProxy(vn5, pvnode); w3.__owl__.pvnode = pvnode; }