[FIX] mail: allow tests to await specific events

task-2333915

closes odoo/odoo#57187

X-original-commit: 699106ce65a87f7a24f04e1d8f232b5a6b4e210a
Related: odoo/enterprise#13018
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Sébastien Theys
2020-09-07 13:16:18 +00:00
parent 88ccd2dfbd
commit 618c77259b
3 changed files with 192 additions and 109 deletions
@@ -3,7 +3,6 @@ odoo.define('mail/static/src/components/discuss/tests/discuss_tests.js', functio
const BusService = require('bus.BusService');
const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js');
const {
afterEach,
afterNextRender,
@@ -23,11 +22,12 @@ QUnit.module('discuss_tests.js', {
beforeEach(this);
this.start = async params => {
const { env, widget } = await start(Object.assign({}, params, {
const { afterEvent, env, widget } = await start(Object.assign({}, params, {
autoOpenDiscuss: true,
data: this.data,
hasDiscuss: true,
}));
this.afterEvent = afterEvent;
this.env = env;
this.widget = widget;
};
@@ -1837,29 +1837,19 @@ QUnit.test('restore thread scroll position', async function (assert) {
res_id: 12,
});
}
const messageFetchChannel11Def = makeDeferred();
const messageFetchChannel12Def = makeDeferred();
await afterNextRender(async () => {
await this.start({
discuss: {
params: {
default_active_id: 'mail.channel_11',
},
await this.start({
discuss: {
params: {
default_active_id: 'mail.channel_11',
},
async mockRPC(route, args) {
const res = await this._super(...arguments);
// domain should be like [['channel_id', 'in', [X]]] with X the channel id
if (route.includes('message_fetch') && args.kwargs.domain[0][2].includes(11)) {
messageFetchChannel11Def.resolve();
}
if (route.includes('message_fetch') && args.kwargs.domain[0][2].includes(12)) {
messageFetchChannel12Def.resolve();
}
return res;
},
waitUntilEvent: {
eventName: 'o-component-message-list-scrolled',
message: "should wait until channel 11 scrolled to its last message",
predicate: ({ threadViewer }) => {
return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 11;
},
});
// wait until messages are fetched, ignore other renders that are too early
await messageFetchChannel11Def;
},
});
assert.strictEqual(
document.querySelectorAll(`
@@ -1869,34 +1859,42 @@ QUnit.test('restore thread scroll position', async function (assert) {
"should have 25 messages in channel 11"
);
// scroll to top of channel11
await afterNextRender(() => {
document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0;
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
func: () => document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0,
message: "should wait until channel 11 changed its scroll position to top",
predicate: ({ threadViewer }) => {
return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 11;
},
});
assert.strictEqual(
document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop,
0,
"should have scrolled to top of thread"
"should have scrolled to top of channel 11",
);
const scrollChannel12Def = makeDeferred();
const onScroll = () => {
scrollChannel12Def.resolve();
};
document.addEventListener('o-message-list-scrolled', onScroll);
// select channel 12
await afterNextRender(async () => {
document.querySelector(`
.o_DiscussSidebar_groupChannel
.o_DiscussSidebar_item[data-thread-local-id="${
this.env.models['mail.thread'].find(thread =>
thread.id === 12 &&
thread.model === 'mail.channel'
).localId
}"]
`).click();
// wait until messages are fetched, ignore other renders that are too early
await messageFetchChannel12Def;
// Ensure scrollIntoView of channel 12 has enough time to complete before
// going back to channel 11. Await is needed to prevent the scrollIntoView
// initially planned for channel 12 to actually apply on channel 11.
// task-2333535
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
func: () => {
// select channel 12
document.querySelector(`
.o_DiscussSidebar_groupChannel
.o_DiscussSidebar_item[data-thread-local-id="${
this.env.models['mail.thread'].find(thread =>
thread.id === 12 &&
thread.model === 'mail.channel'
).localId
}"]
`).click();
},
message: "should wait until channel 12 scrolled to its last message",
predicate: ({ threadViewer }) => {
return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 12;
},
});
assert.strictEqual(
document.querySelectorAll(`
@@ -1906,43 +1904,50 @@ QUnit.test('restore thread scroll position', async function (assert) {
"should have 24 messages in channel 12"
);
// Ensure scrollIntoView of channel 12 has enough time to complete before
// going back to channel 11. Await is needed to prevent the scrollIntoView
// initially planned for channel 12 to actually apply on channel 11.
// task-2333535
await scrollChannel12Def;
document.removeEventListener('o-message-list-scrolled', onScroll);
// select channel 11
await afterNextRender(() =>
document.querySelector(`
.o_DiscussSidebar_groupChannel
.o_DiscussSidebar_item[data-thread-local-id="${
this.env.models['mail.thread'].find(thread =>
thread.id === 11 &&
thread.model === 'mail.channel'
).localId
}"]
`).click()
);
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
func: () => {
// select channel 11
document.querySelector(`
.o_DiscussSidebar_groupChannel
.o_DiscussSidebar_item[data-thread-local-id="${
this.env.models['mail.thread'].find(thread =>
thread.id === 11 &&
thread.model === 'mail.channel'
).localId
}"]
`).click();
},
message: "should wait until channel 11 restored its scroll position",
predicate: ({ threadViewer }) => {
return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 11;
},
});
assert.strictEqual(
document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop,
0,
"should have recovered scroll position of channel 11 (scroll to top)"
);
// select channel12
await afterNextRender(() =>
document.querySelector(`
.o_DiscussSidebar_groupChannel
.o_DiscussSidebar_item[data-thread-local-id="${
this.env.models['mail.thread'].find(thread =>
thread.id === 12 &&
thread.model === 'mail.channel'
).localId
}"]
`).click()
);
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
func: () => {
// select channel 12
document.querySelector(`
.o_DiscussSidebar_groupChannel
.o_DiscussSidebar_item[data-thread-local-id="${
this.env.models['mail.thread'].find(thread =>
thread.id === 12 &&
thread.model === 'mail.channel'
).localId
}"]
`).click();
},
message: "should wait until channel 12 recovered its scroll position",
predicate: ({ threadViewer }) => {
return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 12;
},
});
const messageList = document.querySelector(`
.o_Discuss_thread
.o_ThreadView_messageList
@@ -333,6 +333,9 @@ class MessageList extends Component {
isProcessed = true;
}
if (isProcessed) {
this.env.messagingBus.trigger('o-component-message-list-thread-cache-changed', {
threadViewer: this.threadView.threadViewer,
});
this.threadView.markComponentHintProcessed(hint);
}
}
@@ -397,6 +400,9 @@ class MessageList extends Component {
if (this.props.order === 'asc' && this.props.hasScrollAdjust) {
this.el.scrollTop = this.el.scrollHeight - scrollHeight + scrollTop;
}
this.env.messagingBus.trigger('o-component-message-list-more-messages-loaded', {
threadViewer: this.threadView.threadViewer,
});
this.threadView.markComponentHintProcessed(hint);
}
@@ -526,8 +532,12 @@ class MessageList extends Component {
if (!this.threadView || !this.threadView.threadViewer) {
return;
}
this.trigger('o-message-list-scrolled');
this.threadView.threadViewer.saveThreadCacheScrollPositionsAsInitial(this.el.scrollTop);
const scrollTop = this.el.scrollTop;
this.env.messagingBus.trigger('o-component-message-list-scrolled', {
scrollTop,
threadViewer: this.threadView.threadViewer,
});
this.threadView.threadViewer.saveThreadCacheScrollPositionsAsInitial(scrollTop);
if (!this._isAutoLoadOnScrollActive) {
return;
}
+99 -31
View File
@@ -321,6 +321,12 @@ function beforeEach(self) {
}
function afterEach(self) {
if (self.env) {
self.env.bus.off('hide_home_menu', null);
self.env.bus.off('show_home_menu', null);
self.env.bus.off('will_hide_home_menu', null);
self.env.bus.off('will_show_home_menu', null);
}
// The components must be destroyed before the widget, because the
// widget might destroy the models before destroying the components,
// and the components might still rely on messaging (or other) record(s).
@@ -404,6 +410,11 @@ async function createRootComponent(self, Component, { props = {}, target }) {
* the View class to use in createView.
* @param {Object} [param0.viewOptions] makes only sense when `param0.hasView`
* is set: the view options to use in createView.
* @param {Object} [param0.waitUntilEvent]
* @param {String} [param0.waitUntilEvent.eventName]
* @param {String} [param0.waitUntilEvent.message]
* @param {function} [param0.waitUntilEvent.predicate]
* @param {integer} [param0.waitUntilEvent.timeoutDelay]
* @param {string} [param0.waitUntilMessagingCondition='initialized'] Determines
* the condition of messaging when this function is resolved.
* Supported values: ['none', 'created', 'initialized'].
@@ -438,6 +449,7 @@ async function start(param0 = {}) {
hasTimeControl = false,
hasView = false,
messagingBeforeCreationDeferred = Promise.resolve(),
waitUntilEvent,
waitUntilMessagingCondition = 'initialized',
} = param0;
if (!['none', 'created', 'initialized'].includes(waitUntilMessagingCondition)) {
@@ -594,39 +606,95 @@ async function start(param0 = {}) {
() => testEnv.messagingBus.trigger('will_show_home_menu')
);
const result = { env: testEnv, mockServer, widget };
messagingBeforeCreationDeferred.then(async () => {
/**
* Some models require session data, like locale text direction
* (depends on fully loaded translation).
*/
await env.session.is_bound;
testEnv.modelManager = new ModelManager(testEnv);
testEnv.modelManager.start();
/**
* Create the messaging singleton record.
*/
testEnv.messaging = testEnv.models['mail.messaging'].create();
testEnv.messaging.start().then(() =>
testEnv.messagingInitializedDeferred.resolve()
);
testEnv.messagingCreatedPromise.resolve();
});
if (waitUntilMessagingCondition === 'created') {
await testEnv.messagingCreatedPromise;
}
if (waitUntilMessagingCondition === 'initialized') {
await testEnv.messagingInitializedDeferred;
}
if (mountCallbacks.length > 0) {
await afterNextRender(async () => {
await Promise.all(mountCallbacks.map(callback => callback({ selector, widget })));
/**
* Returns a promise resolved after the expected event is received.
*
* @param {Object} param0
* @param {string} param0.eventName event to wait
* @param {function} param0.func function which, when called, is expected to
* trigger the event
* @param {string} [param0.message] assertion message
* @param {function} [param0.predicate] predicate called with event data.
* If not provided, only the event name has to match.
* @param {number} [param0.timeoutDelay=5000] how long to wait at most in ms
* @returns {Promise}
*/
const afterEvent = (async ({ eventName, func, message, predicate, timeoutDelay = 5000 }) => {
// Set up the timeout to reject if the event is not triggered.
let timeoutNoEvent;
const timeoutProm = new Promise((resolve, reject) => {
timeoutNoEvent = setTimeout(() => {
let error = message
? new Error(message)
: new Error(`Timeout: the event ${eventName} was not triggered.`);
console.error(error);
reject(error);
}, timeoutDelay);
});
// Set up the promise to resolve if the event is triggered.
const eventProm = new Promise(resolve => {
testEnv.messagingBus.on(eventName, null, data => {
if (!predicate || predicate(data)) {
resolve();
}
});
});
// Start the function expected to trigger the event after the
// promise has been registered to not miss any potential event.
const funcRes = func();
// Make them race (first to resolve/reject wins).
await Promise.race([eventProm, timeoutProm]);
clearTimeout(timeoutNoEvent);
// If the event is triggered before the end of the async function,
// ensure the function finishes its job before returning.
await funcRes;
});
const result = {
afterEvent,
env: testEnv,
mockServer,
widget,
};
const start = async () => {
messagingBeforeCreationDeferred.then(async () => {
/**
* Some models require session data, like locale text direction
* (depends on fully loaded translation).
*/
await env.session.is_bound;
testEnv.modelManager = new ModelManager(testEnv);
testEnv.modelManager.start();
/**
* Create the messaging singleton record.
*/
testEnv.messaging = testEnv.models['mail.messaging'].create();
testEnv.messaging.start().then(() =>
testEnv.messagingInitializedDeferred.resolve()
);
testEnv.messagingCreatedPromise.resolve();
});
if (waitUntilMessagingCondition === 'created') {
await testEnv.messagingCreatedPromise;
}
if (waitUntilMessagingCondition === 'initialized') {
await testEnv.messagingInitializedDeferred;
}
if (mountCallbacks.length > 0) {
await afterNextRender(async () => {
await Promise.all(mountCallbacks.map(callback => callback({ selector, widget })));
});
}
returnCallbacks.forEach(callback => callback(result));
};
if (waitUntilEvent) {
await afterEvent(Object.assign({ func: start }, waitUntilEvent));
} else {
await start();
}
returnCallbacks.forEach(callback => callback(result));
return result;
}