The `afternextrender` helper is meant to be used in some tests that want to do something after owl has completed all its work following a specific action. It is useful when the action may cause some additional renders, so waiting for one nextTick is not enough. Unfortunately, it has a bug: it waits for Owl scheduler to be empty. But in some cases, when a component is destroyed because of an error, the scheduler task list will not be cleared until the next render completes. It is implemented that way in Owl to reduce the amount of work it does. But this means that in some testing scenarios, the afterNextRender promise will simply not be resolved, since there is no upcoming render to clear the tasklist. With this commit, we just avoid the issue by ignoring the fibers coming from destroyed component. Note that I think that it is not a good semantic for tests, as it is a little bit too "black-boxy". Ideally, we should rather wait for something in the DOM. There is a waitUntil helper somewhere that may be useful for this situation closes odoo/odoo#115229 X-original-commit: ca8d1e7ea74120dd088c78bb4916aa994e852a0f Signed-off-by: Aaron Bohy (aab) <aab@odoo.com> Signed-off-by: Géry Debongnie <ged@odoo.com>
100 lines
3.5 KiB
JavaScript
100 lines
3.5 KiB
JavaScript
(function () {
|
|
const App = owl.App;
|
|
|
|
const stopPromises = [];
|
|
const schedulers = new Set();
|
|
|
|
function isRunning(scheduler) {
|
|
for (const fiber of scheduler.tasks) {
|
|
if (fiber.node.status !== 2) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function hookIntoScheduler(scheduler) {
|
|
const { flush: originalFlush } = scheduler;
|
|
scheduler.flush = function flush() {
|
|
originalFlush.call(this, ...arguments);
|
|
window.requestAnimationFrame(() => {
|
|
if ([...schedulers].every((scheduler) => !isRunning(scheduler))) {
|
|
while (stopPromises.length) {
|
|
stopPromises.pop().resolve();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
owl.App = class extends App {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.setup();
|
|
hookIntoScheduler(this.scheduler);
|
|
schedulers.add(this.scheduler);
|
|
}
|
|
destroy() {
|
|
schedulers.delete(this.scheduler);
|
|
super.destroy();
|
|
}
|
|
setup() {}
|
|
};
|
|
|
|
/**
|
|
* Returns a promise resolved the next time OWL stops rendering.
|
|
*
|
|
* @param {function} func function which, when called, is
|
|
* expected to trigger OWL render(s).
|
|
* @param {number} [timeoutDelay=5000] in ms
|
|
* @returns {Promise}
|
|
*/
|
|
owl.App.afterNextRender = async function afterNextRender(func, timeoutDelay = 5000) {
|
|
// Define the potential errors outside of the promise to get a proper
|
|
// trace if they happen.
|
|
const startError = new Error("Timeout: the render didn't start.");
|
|
const stopError = new Error("Timeout: the render didn't stop.");
|
|
// Set up the timeout to reject if no render happens.
|
|
let timeoutNoRender;
|
|
|
|
const timeoutProm = new Promise((resolve, reject) => {
|
|
timeoutNoRender = setTimeout(() => {
|
|
let error = startError;
|
|
const runningSchedulers = [...schedulers].filter(isRunning);
|
|
if (runningSchedulers.length) {
|
|
error = stopError;
|
|
}
|
|
console.warn(
|
|
error,
|
|
runningSchedulers.map((s) => [...s.tasks].map((f) => [f, f.node.status])),
|
|
runningSchedulers
|
|
);
|
|
reject(error);
|
|
}, timeoutDelay);
|
|
});
|
|
// Set up the promise to resolve if a render happens.
|
|
let resolve;
|
|
const prom = new Promise((res) => {
|
|
resolve = res;
|
|
});
|
|
prom.resolve = resolve;
|
|
stopPromises.push(prom);
|
|
// Start the function expected to trigger a render after the promise
|
|
// has been registered to not miss any potential render.
|
|
const funcRes = func();
|
|
// Make them race (first to resolve/reject wins).
|
|
await Promise.race([prom, timeoutProm]);
|
|
clearTimeout(timeoutNoRender);
|
|
// Wait the end of the function to ensure all potential effects are
|
|
// taken into account during the following verification step.
|
|
await funcRes;
|
|
// Wait one more frame to make sure no new render has been queued.
|
|
await new Promise(function (resolve) {
|
|
setTimeout(() => window.requestAnimationFrame(() => resolve()));
|
|
});
|
|
if ([...schedulers].some(isRunning)) {
|
|
await afterNextRender(() => {}, timeoutDelay);
|
|
}
|
|
};
|
|
})();
|