[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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user