Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,111 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
patch(MockServer.prototype, 'mail/controllers/discuss', {
/**
* @override
*/
async _performRPC(route, args) {
if (route === '/mail/channel/notify_typing') {
const id = args.channel_id;
const is_typing = args.is_typing;
const context = args.context;
return this._mockRouteMailChannelNotifyTyping(id, is_typing, context);
}
if (route === '/mail/channel/ping') {
return;
}
if (route === '/mail/rtc/channel/join_call') {
return this._mockRouteMailRtcChannelJoinCall(args.channel_id, args.check_rtc_session_ids);
}
if (route === '/mail/rtc/channel/leave_call') {
return this._mockRouteMailRtcChannelLeaveCall(args.channel_id);
}
if (route === '/mail/rtc/session/update_and_broadcast') {
return;
}
return this._super(route, args);
},
/**
* Simulates the `/mail/channel/notify_typing` route.
*
* @private
* @param {integer} channel_id
* @param {integer} limit
* @param {Object} [context={}]
*/
async _mockRouteMailChannelNotifyTyping(channel_id, is_typing, context = {}) {
const partnerId = context.mockedPartnerId || this.currentPartnerId;
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partnerId]]);
if (!memberOfCurrentUser) {
return;
}
this._mockMailChannelMember_NotifyTyping([memberOfCurrentUser.id], is_typing);
},
/**
* Simulates the `/mail/rtc/channel/join_call` route.
*
* @private
* @param {integer} channel_id
* @returns {integer[]} [check_rtc_session_ids]
*/
async _mockRouteMailRtcChannelJoinCall(channel_id, check_rtc_session_ids = []) {
const [currentChannelMember] = this.getRecords('mail.channel.member', [
['channel_id', '=', channel_id],
['partner_id', '=', this.currentPartnerId],
]);
const sessionId = this.pyEnv['mail.channel.rtc.session'].create({
channel_member_id: currentChannelMember.id,
channel_id, // on the server, this is a related field from channel_member_id and not explicitly set
});
const channelMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id]]);
const rtcSessions = this.getRecords('mail.channel.rtc.session', [
['channel_member_id', 'in', channelMembers.map(channelMember => channelMember.id)],
]);
return {
'iceServers': false,
'rtcSessions': [
['insert', rtcSessions.map(rtcSession => this._mockMailChannelRtcSession_MailChannelRtcSessionFormat(rtcSession.id))],
],
'sessionId': sessionId,
};
},
/**
* Simulates the `/mail/rtc/channel/leave_call` route.
*
* @private
* @param {integer} channelId
*/
async _mockRouteMailRtcChannelLeaveCall(channel_id) {
const channelMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id]]);
const rtcSessions = this.getRecords('mail.channel.rtc.session', [
['channel_member_id', 'in', channelMembers.map(channelMember => channelMember.id)],
]);
const notifications = [];
const channelInfo = this._mockMailChannelRtcSession_MailChannelRtcSessionFormatByChannel(rtcSessions.map(rtcSession => rtcSession.id));
for (const [channelId, sessionsData] of Object.entries(channelInfo)) {
const notificationRtcSessions = sessionsData.map((sessionsDataPoint) => {
return { 'id': sessionsDataPoint.id };
});
notifications.push([
channelId,
'mail.channel/rtc_sessions_update',
{
'id': Number(channelId), // JS object keys are strings, but the type from the server is number
'rtcSessions': [['insert-and-unlink', notificationRtcSessions]],
}
]);
}
for (const rtcSession of rtcSessions) {
const target = rtcSession.guest_id || rtcSession.partner_id;
notifications.push([
target,
'mail.channel.rtc.session/ended',
{ 'sessionId': rtcSession.id },
]);
}
this.pyEnv['bus.bus']._sendmany(notifications);
},
});

View file

@ -0,0 +1,25 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
// ensure bus override is applied first.
import "@bus/../tests/helpers/mock_server";
patch(MockServer.prototype, 'mail/models/ir_websocket', {
/**
* Simulates `_get_im_status` on `ir.websocket`.
*
* @param {Object} imStatusIdsByModel
* @param {Number[]|undefined} mail.guest ids of mail.guest whose im_status
* should be monitored.
*/
_mockIrWebsocket__getImStatus(imStatusIdsByModel) {
const imStatus = this._super(imStatusIdsByModel);
const { 'mail.guest': guestIds } = imStatusIdsByModel;
if (guestIds) {
imStatus['guests'] = this.pyEnv['mail.guest'].searchRead([['id', 'in', guestIds]], { context: { 'active_test': false }, fields: ['im_status'] });
}
return imStatus;
},
});

View file

@ -0,0 +1,73 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
patch(MockServer.prototype, 'mail/models/mail_channel_member', {
/**
* Simulates `notify_typing` on `mail.channel.member`.
*
* @private
* @param {integer[]} ids
* @param {boolean} is_typing
*/
_mockMailChannelMember_NotifyTyping(ids, is_typing) {
const members = this.getRecords('mail.channel.member', [['id', 'in', ids]]);
const notifications = [];
for (const member of members) {
const [channel] = this.getRecords('mail.channel', [['id', '=', member.channel_id]]);
const [data] = this._mockMailChannelMember_MailChannelMemberFormat([member.id]);
Object.assign(data, {
'isTyping': is_typing,
});
notifications.push([channel, 'mail.channel.member/typing_status', data]);
notifications.push([channel.uuid, 'mail.channel.member/typing_status', data]);
}
this.pyEnv['bus.bus']._sendmany(notifications);
},
/**
* Simulates `_mail_channel_member_format` on `mail.channel.member`.
*
* @private
* @param {integer[]} ids
* @returns {Object[]}
*/
_mockMailChannelMember_MailChannelMemberFormat(ids) {
const members = this.getRecords('mail.channel.member', [['id', 'in', ids]]);
const dataList = [];
for (const member of members) {
let persona;
if (member.partner_id) {
persona = { 'partner': this._mockMailChannelMember_GetPartnerData([member.id]) };
}
if (member.guest_id) {
const [guest] = this.getRecords('mail.guest', [['id', '=', member.guest_id]]);
persona = {
'guest': {
'id': guest.id,
'im_status': guest.im_status,
'name': guest.name,
},
};
}
const data = {
'channel': { 'id': member.channel_id },
'id': member.id,
'persona': persona,
};
dataList.push(data);
}
return dataList;
},
/**
* Simulates `_get_partner_data` on `mail.channel.member`.
*
* @private
* @param {integer[]} ids
* @returns {Object}
*/
_mockMailChannelMember_GetPartnerData(ids) {
const [member] = this.getRecords('mail.channel.member', [['id', 'in', ids]]);
return this._mockResPartnerMailPartnerFormat([member.partner_id]).get(member.partner_id);
},
});

View file

@ -0,0 +1,43 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
patch(MockServer.prototype, 'mail/models/mail_channel_rtc_session', {
/**
* Simulates `_mail_rtc_session_format` on `mail.channel.rtc.session`.
*
* @private
* @param {integer} id
* @returns {Object}
*/
_mockMailChannelRtcSession_MailChannelRtcSessionFormat(id) {
const [rtcSession] = this.getRecords('mail.channel.rtc.session', [['id', '=', id]]);
return {
'id': rtcSession.id,
'channelMember': this._mockMailChannelMember_MailChannelMemberFormat([rtcSession.channel_member_id])[0],
'isCameraOn': rtcSession.is_camera_on,
'isDeaf': rtcSession.is_deaf,
'isSelfMuted': rtcSession.is_self_muted,
'isScreenSharingOn': rtcSession.is_screen_sharing_on,
};
},
/**
* Simulates `_mail_rtc_session_format` on `mail.channel.rtc.session`.
*
* @private
* @param {integer[]} ids
* @returns {Object}
*/
_mockMailChannelRtcSession_MailChannelRtcSessionFormatByChannel(ids) {
const rtcSessions = this.getRecords('mail.channel.rtc.session', [['id', 'in', ids]]);
const data = {};
for (const rtcSession of rtcSessions) {
if (!data[rtcSession.channel_id]) {
data[rtcSession.channel_id] = [];
}
data[rtcSession.channel_id].push(this._mockMailChannelRtcSession_MailChannelRtcSessionFormat(rtcSession.id));
}
return data;
}
});

View file

@ -0,0 +1,104 @@
/** @odoo-module **/
import { TEST_GROUP_IDS, TEST_USER_IDS } from '@bus/../tests/helpers/test_constants';
import {
addFakeModel,
addModelNamesToFetch,
insertModelFields,
insertRecords
} from '@bus/../tests/helpers/model_definitions_helpers';
//--------------------------------------------------------------------------
// Models
//--------------------------------------------------------------------------
addModelNamesToFetch([
'mail.activity', 'mail.activity.type', 'mail.channel', 'mail.channel.member',
'mail.channel.rtc.session', 'mail.followers', 'mail.guest', 'mail.link.preview', 'mail.message',
'mail.message.subtype', 'mail.notification', 'mail.shortcode', 'mail.template',
'mail.tracking.value', 'res.users.settings', 'res.users.settings.volumes'
]);
addFakeModel('res.fake', {
message_ids: { string: 'Messages', type: 'one2many', relation: 'mail.message' },
activity_ids: { string: "Activities", type: 'one2many', relation: 'mail.activity' },
email_cc: { type: 'char' },
partner_ids: { relation: 'res.partner', string: "Related partners", type: 'one2many' },
});
addFakeModel('m2x.avatar.user', {
user_id: { type: 'many2one', relation: 'res.users' },
user_ids: { type: 'many2many', relation: 'res.users' },
});
//--------------------------------------------------------------------------
// Insertion of fields
//--------------------------------------------------------------------------
insertModelFields('mail.activity', {
chaining_type: { default: 'suggest' },
});
insertModelFields('mail.channel', {
author_id: {
default() {
return this.currentPartnerId;
},
},
avatarCacheKey: { string: "Avatar Cache Key", type: "datetime" },
channel_member_ids: {
default() {
return [[0, 0, { partner_id: this.currentPartnerId }]];
},
},
channel_type: { default: 'channel' },
group_based_subscription: { string: "Group based subscription", type: "boolean" },
group_public_id: {
default() {
return TEST_GROUP_IDS.groupUserId;
},
},
uuid: { default: () => _.uniqueId('mail.channel_uuid-') },
});
insertModelFields('mail.channel.member', {
fold_state: { default: 'open' },
is_pinned: { default: true },
message_unread_counter: { default: 0 },
});
insertModelFields('mail.message', {
author_id: { default: TEST_USER_IDS.currentPartnerId },
history_partner_ids: { relation: 'res.partner', string: "Partners with History", type: 'many2many' },
is_discussion: { string: 'Discussion', type: 'boolean' },
is_note: { string: "Discussion", type: 'boolean' },
is_notification: { string: "Note", type: 'boolean' },
needaction_partner_ids: { relation: 'res.partner', string: "Partners with Need Action", type: 'many2many' },
res_model_name: { string: "Res Model Name", type: 'char' },
});
insertModelFields('mail.message.subtype', {
subtype_xmlid: { type: 'char' },
});
insertModelFields('mail.tracking.value', {
changed_field: { string: 'Changed field', type: 'char' },
new_value: { string: 'New value', type: 'char' },
old_value: { string: 'Old value', type: 'char' },
});
insertModelFields('res.users.settings', {
is_discuss_sidebar_category_channel_open: { default: true },
is_discuss_sidebar_category_chat_open: { default: true },
});
//--------------------------------------------------------------------------
// Insertion of records
//--------------------------------------------------------------------------
insertRecords('mail.activity.type', [
{ icon: 'fa-envelope', id: 1, name: "Email" },
{ icon: 'fa-upload', id: 28, name: "Upload Document" },
]);
insertRecords('mail.message.subtype', [
{ default: false, internal: true, name: "Activities", sequence: 90, subtype_xmlid: 'mail.mt_activities' },
{
default: false, internal: true, name: "Note", sequence: 100, subtype_xmlid: 'mail.mt_note',
track_recipients: true
},
{ name: "Discussions", sequence: 0, subtype_xmlid: 'mail.mt_comment', track_recipients: true },
]);

View file

@ -0,0 +1,106 @@
/** @odoo-module **/
import { browser } from '@web/core/browser/browser';
import { MEDIAS_BREAKPOINTS, SIZES, uiService } from '@web/core/ui/ui_service';
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import config from 'web.config';
/**
* Return the width corresponding to the given size. If an upper and lower bound
* are defined, returns the lower bound: this is an arbitrary choice that should
* not impact anything. A test should pass the `width` parameter instead of `size`
* if it needs a specific width to be set.
*
* @param {number} size
* @returns {number} The width corresponding to the given size.
*/
function getWidthFromSize(size) {
const { minWidth, maxWidth } = MEDIAS_BREAKPOINTS[size];
return minWidth ? minWidth : maxWidth;
}
/**
* Return the size corresponding to the given width.
*
* @param {number} width
* @returns {number} The size corresponding to the given width.
*/
function getSizeFromWidth(width) {
return MEDIAS_BREAKPOINTS.findIndex(({ minWidth, maxWidth }) => {
if (!maxWidth) {
return width >= minWidth;
}
if (!minWidth) {
return width <= maxWidth;
}
return width >= minWidth && width <= maxWidth;
});
}
/**
* Patch legacy objects referring to the ui size. This function must be removed
* when the wowl env will be available in the form_renderer (currently the form
* renderer relies on config). This will impact env.browser.innerWidth,
* env.device.isMobile and config.device.{size_class/isMobile}.
*
* @param {number} size
* @param {number} width
*/
function legacyPatchUiSize(height, size, width) {
const legacyEnv = owl.Component.env;
patchWithCleanup(legacyEnv, {
browser: {
...legacyEnv.browser,
innerWidth: width,
innerHeight: height || browser.innerHeight,
},
device: {
...legacyEnv.device,
isMobile: size <= SIZES.SM,
}
});
patchWithCleanup(config, {
device: {
...config.device,
size_class: size,
isMobile: size <= SIZES.SM,
},
});
}
/**
* Adjust ui size either from given size (mapped to window breakpoints) or
* width. This will impact not only config.device.{size_class/isMobile} but
* uiService.{isSmall/size}, (wowl/legacy) browser.innerWidth, (wowl)
* env.isSmall and (legacy) env.device.isMobile. When a size is given, the browser
* width is set according to the breakpoints that are used by the webClient.
*
* @param {Object} params parameters to configure the ui size.
* @param {number|undefined} [params.size]
* @param {number|undefined} [params.width]
* @param {number|undefined} [params.height]
*/
function patchUiSize({ height, size, width }) {
if (!size && !width || size && width) {
throw new Error('Either size or width must be given to the patchUiSize function');
}
size = size === undefined ? getSizeFromWidth(width) : size;
width = width || getWidthFromSize(size);
patchWithCleanup(browser, {
innerWidth: width,
innerHeight: height || browser.innerHeight,
});
patchWithCleanup(uiService, {
getSize() {
return size;
},
});
legacyPatchUiSize(height, size, width);
}
export {
patchUiSize,
SIZES
};

View file

@ -0,0 +1,398 @@
/** @odoo-module **/
import { getPyEnv, startServer } from '@bus/../tests/helpers/mock_python_environment';
import { nextTick } from '@mail/utils/utils';
import { getAdvanceTime } from '@mail/../tests/helpers/time_control';
import { getWebClientReady } from '@mail/../tests/helpers/webclient_setup';
import { wowlServicesSymbol } from "@web/legacy/utils";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { getFixture, makeDeferred, patchWithCleanup } from "@web/../tests/helpers/utils";
import { doAction, getActionManagerServerData } from "@web/../tests/webclient/helpers";
const { App, EventBus } = owl;
const { afterNextRender } = App;
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* Create a fake object 'dataTransfer', linked to some files,
* which is passed to drag and drop events.
*
* @param {Object[]} files
* @returns {Object}
*/
function _createFakeDataTransfer(files) {
return {
dropEffect: 'all',
effectAllowed: 'all',
files,
items: [],
types: ['Files'],
};
}
//------------------------------------------------------------------------------
// Public: rendering timers
//------------------------------------------------------------------------------
/**
* Returns a promise resolved at the next animation frame.
*
* @returns {Promise}
*/
function nextAnimationFrame() {
return new Promise(function (resolve) {
setTimeout(() => requestAnimationFrame(() => resolve()));
});
}
//------------------------------------------------------------------------------
// Public: test lifecycle
//------------------------------------------------------------------------------
function getAfterEvent({ messagingBus }) {
/**
* 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}
*/
return async function afterEvent({ eventName, func, message, predicate, timeoutDelay = 5000 }) {
const error = new Error(message || `Timeout: the event ${eventName} was not triggered.`);
// Set up the timeout to reject if the event is not triggered.
let timeoutNoEvent;
const timeoutProm = new Promise((resolve, reject) => {
timeoutNoEvent = setTimeout(() => {
console.warn(error);
reject(error);
}, timeoutDelay);
});
// Set up the promise to resolve if the event is triggered.
const eventProm = makeDeferred();
const eventHandler = ev => {
if (!predicate || predicate(ev.detail)) {
eventProm.resolve();
}
};
messagingBus.addEventListener(eventName, eventHandler);
// 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]).finally(() => {
// Execute clean up regardless of whether the promise is
// rejected or not.
clearTimeout(timeoutNoEvent);
messagingBus.removeEventListener(eventName, eventHandler);
});
// If the event is triggered before the end of the async function,
// ensure the function finishes its job before returning.
return await funcRes;
};
}
function getClick({ afterNextRender }) {
return async function click(selector) {
await afterNextRender(() => {
if (typeof selector === "string") {
$(selector)[0].click();
} else if (selector instanceof HTMLElement) {
selector.click();
} else {
// jquery
selector[0].click();
}
});
};
}
function getMouseenter({ afterNextRender }) {
return async function mouseenter(selector) {
await afterNextRender(() =>
document.querySelector(selector).dispatchEvent(new window.MouseEvent('mouseenter'))
);
};
}
function getOpenDiscuss(afterEvent, webClient, { context = {}, params, ...props } = {}) {
return async function openDiscuss({ waitUntilMessagesLoaded = true } = {}) {
const actionOpenDiscuss = {
// hardcoded actionId, required for discuss_container props validation.
id: 104,
context,
params,
tag: 'mail.action_discuss',
type: 'ir.actions.client',
};
if (waitUntilMessagesLoaded) {
let threadId = context.active_id;
if (typeof threadId === 'string') {
threadId = parseInt(threadId.split('_')[1]);
}
return afterNextRender(() => afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => doAction(webClient, actionOpenDiscuss, { props }),
message: "should wait until discuss loaded its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
(!threadId || threadViewer.thread.id === threadId)
);
},
}));
}
return afterNextRender(() => doAction(webClient, actionOpenDiscuss, { props }));
};
}
function getOpenFormView(afterEvent, openView) {
return async function openFormView(action, { props, waitUntilDataLoaded = true, waitUntilMessagesLoaded = true } = {}) {
action['views'] = [[false, 'form']];
const func = () => openView(action, props);
const waitData = func => afterNextRender(() => afterEvent({
eventName: 'o-thread-loaded-data',
func,
message: "should wait until chatter loaded its data",
predicate: ({ thread }) => {
return (
thread.model === action.res_model &&
thread.id === action.res_id
);
},
}));
const waitMessages = func => afterNextRender(() => afterEvent({
eventName: 'o-thread-loaded-messages',
func,
message: "should wait until chatter loaded its messages",
predicate: ({ thread }) => {
return (
thread.model === action.res_model &&
thread.id === action.res_id
);
},
}));
if (waitUntilDataLoaded && waitUntilMessagesLoaded) {
return waitData(() => waitMessages(func));
}
if (waitUntilDataLoaded) {
return waitData(func);
}
if (waitUntilMessagesLoaded) {
return waitMessages(func);
}
return func();
};
}
//------------------------------------------------------------------------------
// Public: start function helpers
//------------------------------------------------------------------------------
/**
* Main function used to make a mocked environment with mocked messaging env.
*
* @param {Object} [param0={}]
* @param {Object} [param0.serverData] The data to pass to the webClient
* @param {Object} [param0.discuss={}] provide data that is passed to the discuss action.
* @param {Object} [param0.legacyServices]
* @param {Object} [param0.services]
* @param {function} [param0.mockRPC]
* @param {boolean} [param0.hasTimeControl=false] if set, all flow of time
* with `messaging.browser.setTimeout` are fully controlled by test itself.
* @param {integer} [param0.loadingBaseDelayDuration=0]
* @param {Deferred|Promise} [param0.messagingBeforeCreationDeferred=Promise.resolve()]
* Deferred that let tests block messaging creation and simulate resolution.
* Useful for testing working components when messaging is not yet created.
* @param {string} [param0.waitUntilMessagingCondition='initialized'] Determines
* the condition of messaging when this function is resolved.
* Supported values: ['none', 'created', 'initialized'].
* - 'none': the function resolves regardless of whether messaging is created.
* - 'created': the function resolves when messaging is created, but
* regardless of whether messaging is initialized.
* - 'initialized' (default): the function resolves when messaging is
* initialized.
* To guarantee messaging is not created, test should pass a pending deferred
* as param of `messagingBeforeCreationDeferred`. To make sure messaging is
* not initialized, test should mock RPC `mail/init_messaging` and block its
* resolution.
* @throws {Error} in case some provided parameters are wrong, such as
* `waitUntilMessagingCondition`.
* @returns {Object}
*/
async function start(param0 = {}) {
// patch _.debounce and _.throttle to be fast and synchronous.
patchWithCleanup(_, {
debounce: func => func,
throttle: func => func,
});
const {
discuss = {},
hasTimeControl,
waitUntilMessagingCondition = 'initialized',
} = param0;
const advanceTime = hasTimeControl ? getAdvanceTime() : undefined;
const target = param0['target'] || getFixture();
param0['target'] = target;
if (!['none', 'created', 'initialized'].includes(waitUntilMessagingCondition)) {
throw Error(`Unknown parameter value ${waitUntilMessagingCondition} for 'waitUntilMessaging'.`);
}
const messagingBus = new EventBus();
const afterEvent = getAfterEvent({ messagingBus });
const pyEnv = await getPyEnv();
param0.serverData = param0.serverData || getActionManagerServerData();
param0.serverData.models = { ...pyEnv.getData(), ...param0.serverData.models };
param0.serverData.views = { ...pyEnv.getViews(), ...param0.serverData.views };
let webClient;
await afterNextRender(async () => {
webClient = await getWebClientReady({ ...param0, messagingBus });
if (waitUntilMessagingCondition === 'created') {
await webClient.env.services.messaging.modelManager.messagingCreatedPromise;
}
if (waitUntilMessagingCondition === 'initialized') {
await webClient.env.services.messaging.modelManager.messagingCreatedPromise;
await webClient.env.services.messaging.modelManager.messagingInitializedPromise;
}
});
registerCleanup(async () => {
await webClient.env.services.messaging.modelManager.messagingInitializedPromise;
webClient.env.services.messaging.modelManager.destroy();
delete webClient.env.services.messaging;
delete owl.Component.env.services.messaging;
delete owl.Component.env[wowlServicesSymbol].messaging;
delete owl.Component.env;
});
const openView = async (action, options) => {
action['type'] = action['type'] || 'ir.actions.act_window';
await afterNextRender(() => doAction(webClient, action, { props: options }));
};
return {
advanceTime,
afterEvent,
afterNextRender,
click: getClick({ afterNextRender }),
env: webClient.env,
insertText,
messaging: webClient.env.services.messaging.modelManager.messaging,
mouseenter: getMouseenter({ afterNextRender }),
openDiscuss: getOpenDiscuss(afterEvent, webClient, discuss),
openView,
openFormView: getOpenFormView(afterEvent, openView),
pyEnv,
webClient,
};
}
//------------------------------------------------------------------------------
// Public: file utilities
//------------------------------------------------------------------------------
/**
* Drag some files over a DOM element
*
* @param {DOM.Element} el
* @param {Object[]} file must have been create beforehand
* @see testUtils.file.createFile
*/
function dragenterFiles(el, files) {
const ev = new Event('dragenter', { bubbles: true });
Object.defineProperty(ev, 'dataTransfer', {
value: _createFakeDataTransfer(files),
});
el.dispatchEvent(ev);
}
/**
* Drop some files on a DOM element
*
* @param {DOM.Element} el
* @param {Object[]} files must have been created beforehand
* @see testUtils.file.createFile
*/
function dropFiles(el, files) {
const ev = new Event('drop', { bubbles: true });
Object.defineProperty(ev, 'dataTransfer', {
value: _createFakeDataTransfer(files),
});
el.dispatchEvent(ev);
}
/**
* Paste some files on a DOM element
*
* @param {DOM.Element} el
* @param {Object[]} files must have been created beforehand
* @see testUtils.file.createFile
*/
function pasteFiles(el, files) {
const ev = new Event('paste', { bubbles: true });
Object.defineProperty(ev, 'clipboardData', {
value: _createFakeDataTransfer(files),
});
el.dispatchEvent(ev);
}
//------------------------------------------------------------------------------
// Public: input utilities
//------------------------------------------------------------------------------
/**
* @param {string} selector
* @param {string} content
*/
async function insertText(selector, content) {
await afterNextRender(() => {
document.querySelector(selector).focus();
for (const char of content) {
document.execCommand('insertText', false, char);
document.querySelector(selector).dispatchEvent(new window.KeyboardEvent('keydown', { key: char }));
document.querySelector(selector).dispatchEvent(new window.KeyboardEvent('keyup', { key: char }));
}
});
}
//------------------------------------------------------------------------------
// Public: DOM utilities
//------------------------------------------------------------------------------
/**
* Determine if a DOM element has been totally scrolled
*
* A 1px margin of error is given to accomodate subpixel rounding issues and
* Element.scrollHeight value being either int or decimal
*
* @param {DOM.Element} el
* @returns {boolean}
*/
function isScrolledToBottom(el) {
return Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) <= 1;
}
//------------------------------------------------------------------------------
// Export
//------------------------------------------------------------------------------
export {
afterNextRender,
dragenterFiles,
dropFiles,
insertText,
isScrolledToBottom,
nextAnimationFrame,
nextTick,
pasteFiles,
start,
startServer,
};

View file

@ -0,0 +1,54 @@
/** @odoo-module **/
import { nextTick } from '@mail/utils/utils';
import { browser } from '@web/core/browser/browser';
import { patchWithCleanup } from "@web/../tests/helpers/utils";
export function getAdvanceTime() {
// list of timeout ids that have timed out.
let timedOutIds = [];
// key: timeoutId, value: func + remaining duration
const timeouts = new Map();
patchWithCleanup(browser, {
clearTimeout: id => {
timeouts.delete(id);
timedOutIds = timedOutIds.filter(i => i !== id);
},
setTimeout: (func, duration) => {
const timeoutId = _.uniqueId('timeout_');
const timeout = {
id: timeoutId,
isTimedOut: false,
func,
duration,
};
timeouts.set(timeoutId, timeout);
if (duration === 0) {
timedOutIds.push(timeoutId);
timeout.isTimedOut = true;
}
return timeoutId;
},
});
return async function (duration) {
await nextTick();
for (const id of timeouts.keys()) {
const timeout = timeouts.get(id);
if (timeout.isTimedOut) {
continue;
}
timeout.duration = Math.max(timeout.duration - duration, 0);
if (timeout.duration === 0) {
timedOutIds.push(id);
}
}
while (timedOutIds.length > 0) {
const id = timedOutIds.shift();
const timeout = timeouts.get(id);
timeouts.delete(id);
timeout.func();
await nextTick();
}
};
}

View file

@ -0,0 +1,144 @@
/** @odoo-module **/
import { busService } from '@bus/services/bus_service';
import { imStatusService } from '@bus/im_status_service';
import { multiTabService } from '@bus/multi_tab_service';
import { makeMultiTabToLegacyEnv } from '@bus/services/legacy/make_multi_tab_to_legacy_env';
import { makeBusServiceToLegacyEnv } from '@bus/services/legacy/make_bus_service_to_legacy_env';
import { makeFakePresenceService } from '@bus/../tests/helpers/mock_services';
import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container';
import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container';
import { DiscussContainer } from '@mail/components/discuss_container/discuss_container';
import { PopoverManagerContainer } from '@mail/components/popover_manager_container/popover_manager_container';
import { messagingService } from '@mail/services/messaging_service';
import { systrayService } from '@mail/services/systray_service';
import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env';
import { registry } from '@web/core/registry';
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { createWebClient } from "@web/../tests/webclient/helpers";
const ROUTES_TO_IGNORE = [
'/web/webclient/load_menus',
'/web/dataset/call_kw/res.users/load_views',
'/web/dataset/call_kw/res.users/systray_get_activities'
];
const WEBCLIENT_PARAMETER_NAMES = new Set(['legacyParams', 'mockRPC', 'serverData', 'target', 'webClientClass']);
const SERVICES_PARAMETER_NAMES = new Set([
'legacyServices', 'loadingBaseDelayDuration', 'messagingBeforeCreationDeferred',
'messagingBus', 'services',
]);
/**
* Add required components to the main component registry.
*/
function setupMainComponentRegistry() {
const mainComponentRegistry = registry.category('main_components');
mainComponentRegistry.add('ChatWindowManagerContainer', { Component: ChatWindowManagerContainer });
mainComponentRegistry.add('DialogManagerContainer', { Component: DialogManagerContainer });
registry.category('actions').add('mail.action_discuss', DiscussContainer);
mainComponentRegistry.add('PopoverManagerContainer', { Component: PopoverManagerContainer });
}
/**
* Setup both legacy and new service registries.
*
* @param {Object} param0
* @param {Object} [param0.services]
* @param {number} [param0.loadingBaseDelayDuration=0]
* @param {Promise} [param0.messagingBeforeCreationDeferred=Promise.resolve()]
* Deferred that let tests block messaging creation and simulate resolution.
* Useful for testing components behavior when messaging is not yet created.
* @param {EventBus} [param0.messagingBus]
* @returns {LegacyRegistry} The registry containing all the legacy services that will be passed
* to the webClient as a legacy parameter.
*/
function setupMessagingServiceRegistries({
loadingBaseDelayDuration = 0,
messagingBeforeCreationDeferred = Promise.resolve(),
messagingBus,
services,
}) {
const serviceRegistry = registry.category('services');
patchWithCleanup(messagingService, {
async _startModelManager(modelManager, messagingValues) {
modelManager.isDebug = true;
const _super = this._super.bind(this);
await messagingBeforeCreationDeferred;
return _super(modelManager, messagingValues);
},
});
const messagingValues = {
start() {
return {
isInQUnitTest: true,
disableAnimation: true,
loadingBaseDelayDuration,
messagingBus,
userNotificationManager: { canPlayAudio: false },
};
}
};
services = {
bus_service: busService,
im_status: imStatusService,
messaging: messagingService,
messagingValues,
presence: makeFakePresenceService({
isOdooFocused: () => true,
}),
systrayService,
multi_tab: multiTabService,
...services,
};
Object.entries(services).forEach(([serviceName, service]) => {
serviceRegistry.add(serviceName, service);
});
registry.category('wowlToLegacyServiceMappers').add('bus_service_to_legacy_env', makeBusServiceToLegacyEnv);
registry.category('wowlToLegacyServiceMappers').add('multi_tab_to_legacy_env', makeMultiTabToLegacyEnv);
registry.category('wowlToLegacyServiceMappers').add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv);
}
/**
* Creates a properly configured instance of WebClient, with the messaging service and all it's
* dependencies initialized.
*
* @param {Object} param0
* @param {Object} [param0.serverData]
* @param {Object} [param0.services]
* @param {Object} [param0.loadingBaseDelayDuration]
* @param {Object} [param0.messagingBeforeCreationDeferred]
* @param {EventBus} [param0.messagingBus] The event bus to be used by messaging.
* @returns {WebClient}
*/
async function getWebClientReady(param0) {
setupMainComponentRegistry();
const servicesParameters = {};
const param0Entries = Object.entries(param0);
for (const [parameterName, value] of param0Entries) {
if (SERVICES_PARAMETER_NAMES.has(parameterName)) {
servicesParameters[parameterName] = value;
}
}
setupMessagingServiceRegistries(servicesParameters);
const webClientParameters = {};
for (const [parameterName, value] of param0Entries) {
if (WEBCLIENT_PARAMETER_NAMES.has(parameterName)) {
webClientParameters[parameterName] = value;
}
}
return createWebClient(webClientParameters);
}
export {
getWebClientReady,
ROUTES_TO_IGNORE,
};

View file

@ -0,0 +1,13 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
registerPatch({
name: 'AutocompleteInputView',
recordMethods: {
onSource(req, res) {
this._super(req, res);
this.messaging.messagingBus.trigger('o-AutocompleteInput-source');
},
},
});

View file

@ -0,0 +1,14 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerPatch({
name: 'ClockWatcher',
fields: {
qunitTestOwner: one('QUnitTest', {
identifying: true,
inverse: 'clockWatcher',
}),
},
});

View file

@ -0,0 +1,101 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
registerPatch({
name: 'EmojiRegistry',
recordMethods: {
async loadEmojiData() {
const dataEmojiCategories = [
{
"name": "Smileys & Emotion",
"title": "🤠",
"sortId": 1
},
{
"name": "People & Body",
"title": "🤟",
"sortId": 2
}];
const dataEmojis = [
{
"codepoints": "😀",
"name": "grinning face",
"shortcodes": [
":grinning:"
],
"emoticons": [],
"category": "Smileys & Emotion",
"keywords": [
"face",
"grin",
"grinning face"
]
},
{
"codepoints": "🤣",
"name": "rolling on the floor laughing",
"shortcodes": [
":rofl:"
],
"emoticons": [],
"category": "Smileys & Emotion",
"keywords": [
"face",
"floor",
"laugh",
"rofl",
"rolling",
"rolling on the floor laughing",
"rotfl"
]
},
{
"codepoints": "😊",
"name": "smiling face with smiling eyes",
"shortcodes": [
":smiling_face_with_smiling_eyes:"
],
"emoticons": [],
"category": "Smileys & Emotion",
"keywords": [
"blush",
"eye",
"face",
"smile",
"smiling face with smiling eyes"
]
},
{
"codepoints": "👋",
"name": "waving hand",
"shortcodes": [
":waving_hand:"
],
"emoticons": [],
"category": "People & Body",
"keywords": [
"hand",
"wave",
"waving"
]
},
{
"codepoints": "🤚",
"name": "raised back of hand",
"shortcodes": [
":raised_back_of_hand:"
],
"emoticons": [],
"category": "People & Body",
"keywords": [
"backhand",
"raised",
"raised back of hand"
]
},
];
this._populateFromEmojiData(dataEmojiCategories, dataEmojis);
},
},
});

View file

@ -0,0 +1,25 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerModel({
name: 'QUnitTest',
fields: {
clockWatcher: one('ClockWatcher', {
inverse: 'qunitTestOwner',
}),
throttle1: one('Throttle', {
inverse: 'qunitTestOwner1',
}),
throttle2: one('Throttle', {
inverse: 'qunitTestOwner2',
}),
timer1: one('Timer', {
inverse: 'qunitTestOwner1',
}),
timer2: one('Timer', {
inverse: 'qunitTestOwner2',
}),
},
});

View file

@ -0,0 +1,66 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, many, one } from '@mail/model/model_field';
registerModel({
name: 'TestAddress',
fields: {
id: attr({
identifying: true,
}),
addressInfo: attr(),
contact: one('TestContact', {
inverse: 'address',
}),
},
});
registerModel({
name: 'TestContact',
fields: {
id: attr({
identifying: true,
}),
address: one('TestAddress', {
inverse: 'contact',
}),
favorite: one('TestHobby', {
default: { description: 'football' },
}),
hobbies: many('TestHobby', {
default: [
{ description: 'hiking' },
{ description: 'fishing' },
],
}),
tasks: many('TestTask', {
inverse: 'responsible'
}),
},
});
registerModel({
name: 'TestHobby',
fields: {
description: attr({
identifying: true,
}),
},
});
registerModel({
name: 'TestTask',
fields: {
id: attr({
identifying: true,
}),
title: attr(),
difficulty: attr({
default: 1,
}),
responsible: one('TestContact', {
inverse: 'tasks'
}),
},
});

View file

@ -0,0 +1,29 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerPatch({
name: 'Throttle',
fields: {
duration: {
compute() {
if (this.qunitTestOwner1) {
return 0;
}
if (this.qunitTestOwner2) {
return 1000;
}
return this._super();
},
},
qunitTestOwner1: one('QUnitTest', {
identifying: true,
inverse: 'throttle1',
}),
qunitTestOwner2: one('QUnitTest', {
identifying: true,
inverse: 'throttle2',
}),
},
});

View file

@ -0,0 +1,29 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerPatch({
name: 'Timer',
fields: {
duration: {
compute() {
if (this.qunitTestOwner1) {
return 0;
}
if (this.qunitTestOwner2) {
return 1000 * 1000;
}
return this._super();
},
},
qunitTestOwner1: one('QUnitTest', {
identifying: true,
inverse: 'timer1',
}),
qunitTestOwner2: one('QUnitTest', {
identifying: true,
inverse: 'timer2',
}),
},
});

View file

@ -0,0 +1,101 @@
/** @odoo-module **/
import { patchUiSize } from '@mail/../tests/helpers/patch_ui_size';
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_mobile_mailbox_selection', {}, function () {
QUnit.module('discuss_mobile_mailbox_selection_tests.js');
QUnit.test('select another mailbox', async function (assert) {
assert.expect(7);
patchUiSize({ height: 360, width: 640 });
const { click, messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
'.o_Discuss',
"should display discuss initially"
);
assert.hasClass(
document.querySelector('.o_Discuss'),
'o-isDeviceSmall',
"discuss should be opened in mobile mode"
);
assert.containsOnce(
document.body,
'.o_Discuss_thread',
"discuss should display a thread initially"
);
assert.strictEqual(
document.querySelector('.o_Discuss_thread').dataset.threadId,
messaging.inbox.thread.id,
"inbox mailbox should be opened initially"
);
assert.containsOnce(
document.body,
`.o_DiscussMobileMailboxSelectionItem[
data-mailbox-local-id="${messaging.starred.localId}"
]`,
"should have a button to open starred mailbox"
);
await click(`.o_DiscussMobileMailboxSelectionItem[
data-mailbox-local-id="${messaging.starred.localId}"]
`);
assert.containsOnce(
document.body,
'.o_Discuss_thread',
"discuss should still have a thread after clicking on starred mailbox"
);
assert.strictEqual(
document.querySelector('.o_Discuss_thread').dataset.threadId,
messaging.starred.thread.id,
"starred mailbox should be opened after clicking on it"
);
});
QUnit.test('auto-select "Inbox" when discuss had channel as active thread', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
patchUiSize({ height: 360, width: 640 });
const { click, messaging, openDiscuss } = await start({
discuss: {
context: {
active_id: mailChannelId1,
},
},
});
await openDiscuss({ waitUntilMessagesLoaded: false });
assert.hasClass(
document.querySelector('.o_MobileMessagingNavbar_tab[data-tab-id="channel"]'),
'o-active',
"'channel' tab should be active initially when loading discuss with channel id as active_id"
);
await click('.o_MobileMessagingNavbar_tab[data-tab-id="mailbox"]');
assert.hasClass(
document.querySelector('.o_MobileMessagingNavbar_tab[data-tab-id="mailbox"]'),
'o-active',
"'mailbox' tab should be selected after click on mailbox tab"
);
assert.hasClass(
document.querySelector(`.o_DiscussMobileMailboxSelectionItem[data-mailbox-local-id="${
messaging.inbox.localId
}"]`),
'o-active',
"'Inbox' mailbox should be auto-selected after click on mailbox tab"
);
});
});
});
});

View file

@ -0,0 +1,480 @@
/** @odoo-module **/
import { nextAnimationFrame, start, startServer } from '@mail/../tests/helpers/test_utils';
import { ROUTES_TO_IGNORE } from '@mail/../tests/helpers/webclient_setup';
import testUtils from 'web.test_utils';
import { patchDate, patchWithCleanup, selectDropdownItem, editInput } from '@web/../tests/helpers/utils';
import { ListController } from "@web/views/list/list_controller";
QUnit.module('mail', {}, function () {
QUnit.module('Chatter');
QUnit.test('list activity widget with no activity', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const views = {
'res.users,false,list': '<list><field name="activity_ids" widget="list_activity"/></list>',
};
const { openView } = await start({
mockRPC: function (route, args) {
if (
args.method !== 'get_views' &&
!['/mail/init_messaging', '/mail/load_message_failures', '/bus/im_status', ...ROUTES_TO_IGNORE].includes(route)
) {
assert.step(route);
}
},
serverData: { views },
session: { uid: pyEnv.currentUserId },
});
await openView({
res_model: 'res.users',
views: [[false, 'list']],
});
assert.containsOnce(document.body, '.o_ActivityButtonView_icon.text-muted');
assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, '');
assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']);
});
QUnit.test('list activity widget with activities', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const [mailActivityId1, mailActivityId2] = pyEnv['mail.activity'].create([{}, {}]);
const [mailActivityTypeId1, mailActivityTypeId2] = pyEnv['mail.activity.type'].create(
[{ name: 'Type 1' }, { name: 'Type 2' }],
);
pyEnv['res.users'].write([pyEnv.currentUserId], {
activity_ids: [mailActivityId1, mailActivityId2],
activity_state: 'today',
activity_summary: 'Call with Al',
activity_type_id: mailActivityTypeId1,
activity_type_icon: 'fa-phone',
});
pyEnv['res.users'].create({
activity_ids: [mailActivityId2],
activity_state: 'planned',
activity_summary: false,
activity_type_id: mailActivityTypeId2,
});
const views = {
'res.users,false,list': '<list><field name="activity_ids" widget="list_activity"/></list>',
};
const { openView } = await start({
mockRPC: function (route, args) {
if (
args.method !== 'get_views' &&
!['/mail/init_messaging', '/mail/load_message_failures', '/bus/im_status', ...ROUTES_TO_IGNORE].includes(route)
) {
assert.step(route);
}
},
serverData: { views },
});
await openView({
res_model: 'res.users',
views: [[false, 'list']],
});
const firstRow = document.querySelector('.o_data_row');
assert.containsOnce(firstRow, '.o_ActivityButtonView_icon.text-warning.fa-phone');
assert.strictEqual(firstRow.querySelector('.o_ListFieldActivityView_summary').innerText, 'Call with Al');
const secondRow = document.querySelectorAll('.o_data_row')[1];
assert.containsOnce(secondRow, '.o_ActivityButtonView_icon.text-success.fa-clock-o');
assert.strictEqual(secondRow.querySelector('.o_ListFieldActivityView_summary').innerText, 'Type 2');
assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']);
});
QUnit.test('list activity widget with activities, two pages, mark done', async function (assert) {
patchDate(2023, 0, 11, 12, 0, 0);
const pyEnv = await startServer();
const mailActivityTypeId = pyEnv['mail.activity.type'].create({});
const mailActivityId = pyEnv['mail.activity'].create({
display_name: "Meet FP",
date_deadline: moment().add(1, 'day').format("YYYY-MM-DD"), // tomorrow
can_write: true,
state: "planned",
user_id: pyEnv.currentUserId,
create_uid: pyEnv.currentUserId,
activity_type_id: mailActivityTypeId,
});
pyEnv['res.users'].create({ display_name: "User 1"});
pyEnv['res.users'].create({ display_name: "User 2"});
pyEnv['res.users'].create({
display_name: "User 3",
activity_ids: [mailActivityId],
activity_state: 'planned',
activity_summary: "Something to do",
activity_type_id: mailActivityTypeId,
});
const views = {
'res.users,false,list': `
<list limit="2">
<field name="activity_ids" widget="list_activity"/>
</list>`,
};
const { click, openView } = await start({
serverData: { views },
});
await openView({
res_model: 'res.users',
views: [[false, 'list']],
});
assert.containsOnce(document.body, ".o_list_view");
assert.strictEqual(document.querySelector(".o_cp_pager").innerText, "1-2 / 4");
await click(document.querySelector(".o_pager_next"));
assert.strictEqual(document.querySelector(".o_cp_pager").innerText, "3-4 / 4");
assert.strictEqual(document.querySelectorAll(".o_data_row")[1].querySelector("[name=activity_ids]").innerText, "Something to do");
await click(document.querySelectorAll(".o_ActivityButtonView")[1]);
await click(document.querySelector(".o_ActivityListViewItem_markAsDone"));
await click(document.querySelector(".o_ActivityMarkDonePopoverContent_doneButton"));
assert.strictEqual(document.querySelector(".o_cp_pager").innerText, "3-4 / 4");
assert.strictEqual(document.querySelectorAll(".o_data_row")[1].querySelector("[name=activity_ids]").innerText, "");
});
QUnit.test('list activity widget with exception', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const mailActivityId1 = pyEnv['mail.activity'].create({});
const mailActivityTypeId1 = pyEnv['mail.activity.type'].create({});
pyEnv['res.users'].write([pyEnv.currentUserId], {
activity_ids: [mailActivityId1],
activity_state: 'today',
activity_summary: 'Call with Al',
activity_type_id: mailActivityTypeId1,
activity_exception_decoration: 'warning',
activity_exception_icon: 'fa-warning',
});
const views = {
'res.users,false,list': '<list><field name="activity_ids" widget="list_activity"/></list>',
};
const { openView } = await start({
mockRPC: function (route, args) {
if (
args.method !== 'get_views' &&
!['/mail/init_messaging', '/mail/load_message_failures', '/bus/im_status', ...ROUTES_TO_IGNORE].includes(route)
) {
assert.step(route);
}
},
serverData: { views },
});
await openView({
res_model: 'res.users',
views: [[false, 'list']],
});
assert.containsOnce(document.body, '.o_ActivityButtonView_icon.text-warning.fa-warning');
assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, 'Warning');
assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']);
});
QUnit.test('list activity widget: open dropdown', async function (assert) {
assert.expect(9);
const pyEnv = await startServer();
const [mailActivityTypeId1, mailActivityTypeId2] = pyEnv['mail.activity.type'].create([{}, {}]);
const [mailActivityId1, mailActivityId2] = pyEnv['mail.activity'].create([
{
display_name: "Call with Al",
date_deadline: moment().format("YYYY-MM-DD"), // now
can_write: true,
state: "today",
user_id: pyEnv.currentUserId,
create_uid: pyEnv.currentUserId,
activity_type_id: mailActivityTypeId1,
},
{
display_name: "Meet FP",
date_deadline: moment().add(1, 'day').format("YYYY-MM-DD"), // tomorrow
can_write: true,
state: "planned",
user_id: pyEnv.currentUserId,
create_uid: pyEnv.currentUserId,
activity_type_id: mailActivityTypeId2,
}
]);
pyEnv['res.users'].write([pyEnv.currentUserId], {
activity_ids: [mailActivityId1, mailActivityId2],
activity_state: 'today',
activity_summary: 'Call with Al',
activity_type_id: mailActivityTypeId2,
});
const views = {
'res.users,false,list': '<list><field name="activity_ids" widget="list_activity"/></list>',
};
const { click, openView } = await start({
mockRPC: function (route, args) {
if (
args.method !== 'get_views' &&
!['/mail/init_messaging', '/mail/load_message_failures', '/bus/im_status', ...ROUTES_TO_IGNORE].includes(route)
) {
assert.step(args.method || route);
}
if (args.method === 'action_feedback') {
pyEnv['res.users'].write([pyEnv.currentUserId], {
activity_ids: [mailActivityId2],
activity_state: 'planned',
activity_summary: 'Meet FP',
activity_type_id: mailActivityTypeId1,
});
// random value returned in order for the mock server to know that this route is implemented.
return true;
}
},
serverData: { views },
});
patchWithCleanup(ListController.prototype, {
setup() {
this._super();
const selectRecord = this.props.selectRecord;
this.props.selectRecord = (...args) => {
assert.step(`select_record ${JSON.stringify(args)}`);
return selectRecord(...args);
};
}
});
await openView({
res_model: 'res.users',
views: [[false, 'list']],
});
assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, 'Call with Al');
await click('.o_ActivityButtonView'); // open the popover
await click('.o_ActivityListViewItem_markAsDone'); // mark the first activity as done
await click('.o_ActivityMarkDonePopoverContent_doneButton'); // confirm
assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, 'Meet FP');
assert.verifySteps([
'web_search_read',
'activity_format',
'action_feedback',
'/mail/thread/messages',
'/mail/thread/data',
'web_search_read',
]);
});
QUnit.test('list activity exception widget with activity', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const [mailActivityTypeId1, mailActivityTypeId2] = pyEnv['mail.activity.type'].create([{}, {}]);
const [mailActivityId1, mailActivityId2] = pyEnv['mail.activity'].create([
{
display_name: "An activity",
date_deadline: moment().format("YYYY-MM-DD"), // now
can_write: true,
state: "today",
user_id: pyEnv.currentUserId,
create_uid: pyEnv.currentUserId,
activity_type_id: mailActivityTypeId1,
},
{
display_name: "An exception activity",
date_deadline: moment().format("YYYY-MM-DD"), // now
can_write: true,
state: "today",
user_id: pyEnv.currentUserId,
create_uid: pyEnv.currentUserId,
activity_type_id: mailActivityTypeId2,
}
]);
pyEnv['res.users'].write([pyEnv.currentUserId], { activity_ids: [mailActivityId1] });
pyEnv['res.users'].create({
message_attachment_count: 3,
display_name: "second partner",
message_follower_ids: [],
message_ids: [],
activity_ids: [mailActivityId2],
activity_exception_decoration: 'warning',
activity_exception_icon: 'fa-warning',
});
const views = {
'res.users,false,list':
`<tree>
<field name="activity_exception_decoration" widget="activity_exception"/>
</tree>`,
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'res.users',
views: [[false, 'list']],
});
assert.containsN(document.body, '.o_data_row', 2, "should have two records");
assert.containsNone(document.querySelectorAll('.o_data_row .o_activity_exception_cell')[0], '.o_ActivityException', "there is no any exception activity on record");
assert.containsOnce(document.querySelectorAll('.o_data_row .o_activity_exception_cell')[1], '.o_ActivityException', "there is an exception on a record");
});
QUnit.module('FieldMany2ManyTagsEmail');
QUnit.test('fieldmany2many tags email (edition)', async function (assert) {
assert.expect(17);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([
{ name: "gold", email: 'coucou@petite.perruche' },
{ name: "silver", email: '' },
]);
const mailMessageId1 = pyEnv['mail.message'].create({
partner_ids: [resPartnerId1],
});
const views = {
'mail.message,false,form':
'<form string="Partners">' +
'<sheet>' +
'<field name="body"/>' +
'<field name="partner_ids" widget="many2many_tags_email"/>' +
'</sheet>' +
'</form>',
'res.partner,false,form': '<form string="Types"><field name="name"/><field name="email"/></form>',
};
var { openView } = await start({
serverData: { views },
mockRPC: function (route, args) {
if (args.method === 'read' && args.model === 'res.partner') {
assert.step(JSON.stringify(args.args[0]));
assert.ok(args.args[1].includes('email'), "should read the email");
} else if (args.method === "get_formview_id") {
return false;
}
},
});
await openView(
{
res_id: mailMessageId1,
res_model: 'mail.message',
views: [[false, 'form']],
},
{
mode: 'edit',
},
);
assert.verifySteps([`[${resPartnerId1}]`]);
assert.containsOnce(document.body, '.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0',
"should contain one tag");
// add an other existing tag
await selectDropdownItem(document.body, 'partner_ids', "silver");
assert.strictEqual(document.querySelectorAll('.modal-content .o_form_view').length, 1,
"there should be one modal opened to edit the empty email");
assert.strictEqual(document.querySelector(".modal-content .o_form_view .o_input#name").value, "silver",
"the opened modal in edit mode should be a form view dialog with the res.partner 14");
assert.strictEqual(document.querySelectorAll(".modal-content .o_form_view .o_input#email").length, 1,
"there should be an email field in the modal");
// set the email and save the modal (will rerender the form view)
await testUtils.fields.editInput($('.modal-content .o_form_view .o_input#email'), 'coucou@petite.perruche');
await testUtils.dom.click($('.modal-content .o_form_button_save'));
assert.containsN(document.body, '.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0', 2,
"should contain the second tag");
const firstTag = document.querySelector('.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0');
assert.strictEqual(firstTag.querySelector('.o_badge_text').innerText, "gold",
"tag should only show name");
assert.hasAttrValue(firstTag.querySelector('.o_badge_text'), 'title', "coucou@petite.perruche",
"tag should show email address on mouse hover");
// should have read resPartnerId2 three times: when opening the dropdown, when opening the modal, and
// after the save
assert.verifySteps([`[${resPartnerId2}]`, `[${resPartnerId2}]`, `[${resPartnerId2}]`]);
});
QUnit.test('many2many_tags_email widget can load more than 40 records', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const messagePartnerIds = [];
for (let i = 100; i < 200; i++) {
messagePartnerIds.push(pyEnv['res.partner'].create({ display_name: `partner${i}` }));
}
const mailMessageId1 = pyEnv['mail.message'].create({
partner_ids: messagePartnerIds,
});
const views = {
'mail.message,false,form': '<form><field name="partner_ids" widget="many2many_tags"/></form>',
};
var { openView } = await start({
serverData: { views },
});
await openView({
res_id: mailMessageId1,
res_model: 'mail.message',
views: [[false, 'form']],
});
assert.strictEqual(document.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length, 100);
assert.containsOnce(document.body, '.o_form_editable');
// add a record to the relation
await selectDropdownItem(document.body, 'partner_ids', "Public user");
assert.strictEqual(document.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length, 101);
});
QUnit.test("auto save on click of activity widget in list view", async (assert) => {
const pyEnv = await startServer();
const activityId = pyEnv["mail.activity"].create({});
pyEnv["res.users"].write([pyEnv.currentUserId], {
activity_ids: [activityId],
activity_state: "today",
});
const { click, openView } = await start({
mockRPC(route) {
if (route === "/web/dataset/call_kw/res.users/create") {
pyEnv["res.users"].create({ activity_ids: [activityId] });
assert.step(route);
}
},
serverData: {
views: {
"res.users,false,list": `
<list editable="bottom">
<field name="name" required="1"/>
<field name="activity_ids" widget="list_activity"/>
</list>`,
}
},
});
await openView({
res_model: "res.users",
views: [[false, "list"]],
});
await click(".o_list_button_add");
assert.containsOnce($, ".o_selected_row .fa-clock-o");
click(".o_selected_row .fa-clock-o").catch(() => {});
await nextAnimationFrame();
assert.containsOnce($, ".o_notification:contains(Invalid fields: Name)");
await editInput($(".o_selected_row")[0], "[name=name] input", "tommy");
await click(".o_selected_row .fa-clock-o");
assert.verifySteps(["/web/dataset/call_kw/res.users/create"]);
});
});

View file

@ -0,0 +1,295 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('activity_mark_done_popover_tests.js');
QUnit.test('activity mark done popover simplest layout', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['mail.activity'].create({
activity_category: 'not_upload_file',
can_write: true,
res_id: resPartnerId1,
res_model: 'res.partner',
});
const { click, openView } = await start();
await openView({
res_model: 'res.partner',
res_id: resPartnerId1,
views: [[false, 'form']],
});
await click('.o_Activity_markDoneButton');
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent',
"Popover component should be present"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent_feedback',
"Popover component should contain the feedback textarea"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent_buttons',
"Popover component should contain the action buttons"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent_doneScheduleNextButton',
"Popover component should contain the done & schedule next button"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent_doneButton',
"Popover component should contain the done button"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent_discardButton',
"Popover component should contain the discard button"
);
});
QUnit.test('activity with force next mark done popover simplest layout', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['mail.activity'].create({
activity_category: 'not_upload_file',
can_write: true,
chaining_type: 'trigger',
res_id: resPartnerId1,
res_model: 'res.partner',
});
const { click, openView } = await start();
await openView({
res_model: 'res.partner',
res_id: resPartnerId1,
views: [[false, 'form']],
});
await click('.o_Activity_markDoneButton');
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent',
"Popover component should be present"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent_feedback',
"Popover component should contain the feedback textarea"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent_buttons',
"Popover component should contain the action buttons"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent_doneScheduleNextButton',
"Popover component should contain the done & schedule next button"
);
assert.containsNone(
document.body,
'.o_ActivityMarkDonePopoverContent_doneButton',
"Popover component should NOT contain the done button"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopoverContent_discardButton',
"Popover component should contain the discard button"
);
});
QUnit.test('activity mark done popover mark done without feedback', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailActivityId1 = pyEnv['mail.activity'].create({
activity_category: 'not_upload_file',
can_write: true,
res_id: resPartnerId1,
res_model: 'res.partner',
});
const { click, openView } = await start({
async mockRPC(route, args) {
if (route === '/web/dataset/call_kw/mail.activity/action_feedback') {
assert.step('action_feedback');
assert.strictEqual(args.args.length, 1);
assert.strictEqual(args.args[0].length, 1);
assert.strictEqual(args.args[0][0], mailActivityId1);
assert.strictEqual(args.kwargs.attachment_ids.length, 0);
assert.notOk(args.kwargs.feedback);
// random value returned in order for the mock server to know that this route is implemented.
return true;
}
if (route === '/web/dataset/call_kw/mail.activity/unlink') {
// 'unlink' on non-existing record raises a server crash
throw new Error("'unlink' RPC on activity must not be called (already unlinked from mark as done)");
}
},
});
await openView({
res_model: 'res.partner',
res_id: resPartnerId1,
views: [[false, 'form']],
});
await click('.o_Activity_markDoneButton');
await click('.o_ActivityMarkDonePopoverContent_doneButton');
assert.verifySteps(
['action_feedback'],
"Mark done and schedule next button should call the right rpc"
);
});
QUnit.test('activity mark done popover mark done with feedback', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailActivityId1 = pyEnv['mail.activity'].create({
activity_category: 'not_upload_file',
can_write: true,
res_id: resPartnerId1,
res_model: 'res.partner',
});
const { click, openView } = await start({
async mockRPC(route, args) {
if (route === '/web/dataset/call_kw/mail.activity/action_feedback') {
assert.step('action_feedback');
assert.strictEqual(args.args.length, 1);
assert.strictEqual(args.args[0].length, 1);
assert.strictEqual(args.args[0][0], mailActivityId1);
assert.strictEqual(args.kwargs.attachment_ids.length, 0);
assert.strictEqual(args.kwargs.feedback, 'This task is done');
// random value returned in order for the mock server to know that this route is implemented.
return true;
}
if (route === '/web/dataset/call_kw/mail.activity/unlink') {
// 'unlink' on non-existing record raises a server crash
throw new Error("'unlink' RPC on activity must not be called (already unlinked from mark as done)");
}
},
});
await openView({
res_model: 'res.partner',
res_id: resPartnerId1,
views: [[false, 'form']],
});
await click('.o_Activity_markDoneButton');
let feedbackTextarea = document.querySelector('.o_ActivityMarkDonePopoverContent_feedback');
feedbackTextarea.focus();
document.execCommand('insertText', false, 'This task is done');
document.querySelector('.o_ActivityMarkDonePopoverContent_doneButton').click();
assert.verifySteps(
['action_feedback'],
"Mark done and schedule next button should call the right rpc"
);
});
QUnit.test('activity mark done popover mark done and schedule next', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailActivityId1 = pyEnv['mail.activity'].create({
activity_category: 'not_upload_file',
can_write: true,
res_id: resPartnerId1,
res_model: 'res.partner',
});
const { click, env, openView } = await start({
async mockRPC(route, args) {
if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') {
assert.step('action_feedback_schedule_next');
assert.strictEqual(args.args.length, 1);
assert.strictEqual(args.args[0].length, 1);
assert.strictEqual(args.args[0][0], mailActivityId1);
assert.strictEqual(args.kwargs.feedback, 'This task is done');
return false;
}
if (route === '/web/dataset/call_kw/mail.activity/unlink') {
// 'unlink' on non-existing record raises a server crash
throw new Error("'unlink' RPC on activity must not be called (already unlinked from mark as done)");
}
},
});
await openView({
res_model: 'res.partner',
res_id: resPartnerId1,
views: [[false, 'form']],
});
patchWithCleanup(env.services.action, {
doAction() {
assert.step('activity_action');
throw new Error("The do-action event should not be triggered when the route doesn't return an action");
},
});
await click('.o_Activity_markDoneButton');
let feedbackTextarea = document.querySelector('.o_ActivityMarkDonePopoverContent_feedback');
feedbackTextarea.focus();
document.execCommand('insertText', false, 'This task is done');
await click('.o_ActivityMarkDonePopoverContent_doneScheduleNextButton');
assert.verifySteps(
['action_feedback_schedule_next'],
"Mark done and schedule next button should call the right rpc and not trigger an action"
);
});
QUnit.test('[technical] activity mark done & schedule next with new action', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['mail.activity'].create({
activity_category: 'not_upload_file',
can_write: true,
res_id: resPartnerId1,
res_model: 'res.partner',
});
const { click, env, openView } = await start({
async mockRPC(route, args) {
if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') {
return { type: 'ir.actions.act_window' };
}
},
});
await openView({
res_model: 'res.partner',
res_id: resPartnerId1,
views: [[false, 'form']],
});
patchWithCleanup(env.services.action, {
doAction(action) {
assert.step('activity_action');
assert.deepEqual(
action,
{ type: 'ir.actions.act_window' },
"The content of the action should be correct"
);
},
});
await click('.o_Activity_markDoneButton');
await click('.o_ActivityMarkDonePopoverContent_doneScheduleNextButton');
assert.verifySteps(
['activity_action'],
"The action returned by the route should be executed"
);
});
});
});

View file

@ -0,0 +1,271 @@
/** @odoo-module **/
import { nextAnimationFrame, start, startServer } from '@mail/../tests/helpers/test_utils';
import { patchUiSize, SIZES } from '@mail/../tests/helpers/patch_ui_size';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('attachment_box_tests.js');
QUnit.test('base empty rendering', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const views = {
'res.partner,false,form':
`<form>
<div class="oe_chatter">
<field name="message_ids" options="{'open_attachments': True}"/>
</div>
</form>`,
};
const { messaging, openView } = await start({ serverData: { views } });
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentBox`).length,
1,
"should have an attachment box"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentBox_buttonAdd`).length,
1,
"should have a button add"
);
assert.ok(
messaging.models['Chatter'].all()[0].attachmentBoxView.fileUploader,
"should have a file uploader"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentBox .o_AttachmentCard`).length,
0,
"should not have any attachment"
);
});
QUnit.test('base non-empty rendering', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['ir.attachment'].create([
{
mimetype: 'text/plain',
name: 'Blah.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
{
mimetype: 'text/plain',
name: 'Blu.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
]);
const views = {
'res.partner,false,form':
`<form>
<div class="oe_chatter">
<field name="message_ids" options="{'open_attachments': True}"/>
</div>
</form>`,
};
const { messaging, openView } = await start({ serverData: { views } });
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentBox`).length,
1,
"should have an attachment box"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentBox_buttonAdd`).length,
1,
"should have a button add"
);
assert.ok(
messaging.models['Chatter'].all()[0].attachmentBoxView.fileUploader,
"should have a file uploader"
);
assert.strictEqual(
document.querySelectorAll(`.o_attachmentBox_attachmentList`).length,
1,
"should have an attachment list"
);
});
QUnit.test('view attachments', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const [irAttachmentId1] = pyEnv['ir.attachment'].create([
{
mimetype: 'text/plain',
name: 'Blah.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
{
mimetype: 'text/plain',
name: 'Blu.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
]);
const views = {
'res.partner,false,form':
`<form>
<div class="oe_chatter">
<field name="message_ids" options="{'open_attachments': True}"/>
</div>
</form>`,
};
const { click, messaging, openView } = await start({ serverData: { views } });
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
const firstAttachment = messaging.models['Attachment'].findFromIdentifyingData({ id: irAttachmentId1 });
await click(`
.o_AttachmentCard[data-id="${firstAttachment.localId}"]
.o_AttachmentCard_image
`);
assert.containsOnce(
document.body,
'.o_Dialog',
"a dialog should have been opened once attachment image is clicked",
);
assert.containsOnce(
document.body,
'.o_AttachmentViewer',
"an attachment viewer should have been opened once attachment image is clicked",
);
assert.strictEqual(
document.querySelector('.o_AttachmentViewer_name').textContent,
'Blah.txt',
"attachment viewer iframe should point to clicked attachment",
);
assert.containsOnce(
document.body,
'.o_AttachmentViewer_buttonNavigationNext',
"attachment viewer should allow to see next attachment",
);
await click('.o_AttachmentViewer_buttonNavigationNext');
assert.strictEqual(
document.querySelector('.o_AttachmentViewer_name').textContent,
'Blu.txt',
"attachment viewer iframe should point to next attachment of attachment box",
);
assert.containsOnce(
document.body,
'.o_AttachmentViewer_buttonNavigationNext',
"attachment viewer should allow to see next attachment",
);
await click('.o_AttachmentViewer_buttonNavigationNext');
assert.strictEqual(
document.querySelector('.o_AttachmentViewer_name').textContent,
'Blah.txt',
"attachment viewer iframe should point anew to first attachment",
);
});
QUnit.test('remove attachment should ask for confirmation', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['ir.attachment'].create({
mimetype: 'text/plain',
name: 'Blah.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
});
const views = {
'res.partner,false,form':
`<form>
<div class="oe_chatter">
<field name="message_ids" options="{'open_attachments': True}"/>
</div>
</form>`,
};
const { click, openView } = await start({ serverData: { views } });
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_AttachmentCard',
"should have an attachment",
);
assert.containsOnce(
document.body,
'.o_AttachmentCard_asideItemUnlink',
"attachment should have a delete button"
);
await click('.o_AttachmentCard_asideItemUnlink');
assert.containsOnce(
document.body,
'.o_AttachmentDeleteConfirm',
"A confirmation dialog should have been opened"
);
assert.strictEqual(
document.querySelector('.o_AttachmentDeleteConfirm_mainText').textContent,
`Do you really want to delete "Blah.txt"?`,
"Confirmation dialog should contain the attachment delete confirmation text"
);
// Confirm the deletion
await click('.o_AttachmentDeleteConfirm_confirmButton');
assert.containsNone(
document.body,
'.o_AttachmentCard',
"should no longer have an attachment",
);
});
QUnit.test("scroll to attachment box when toggling on", async function (assert) {
patchUiSize({ size: SIZES.XXL });
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({});
for (let i = 0; i < 30; i++) {
pyEnv["mail.message"].create({
body: "not empty".repeat(50),
model: "res.partner",
res_id: partnerId,
});
}
pyEnv["ir.attachment"].create({
mimetype: "text/plain",
name: "Blah.txt",
res_id: partnerId,
res_model: "res.partner",
});
const { click, openView } = await start();
await openView({
res_id: partnerId,
res_model: "res.partner",
views: [[false, "form"]],
});
$(".o_Chatter_scrollPanel").scrollTop(10 * 1000); // to bottom
await nextAnimationFrame();
await click(".o_ChatterTopbar_buttonToggleAttachments");
assert.strictEqual($(".o_Chatter_scrollPanel").scrollTop(), 0);
});
});
});

View file

@ -0,0 +1,51 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('attachment_image_tests.js');
QUnit.test('auto layout with image', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.png",
mimetype: 'image/png',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentImage img`).length,
1,
"attachment should have an image part"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentImage_imageOverlay`).length,
1,
"attachment should have an image overlay part"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentImage_aside`).length,
0,
"attachment should not have an aside element"
);
});
});
});

View file

@ -0,0 +1,530 @@
/** @odoo-module **/
import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils';
import { contains } from "@web/../tests/utils";
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('attachment_list_tests.js');
QUnit.test('simplest layout', async function (assert) {
assert.expect(8);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.txt",
mimetype: 'text/plain',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { messaging, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
assert.strictEqual(
document.querySelectorAll('.o_AttachmentList').length,
1,
"should have attachment list component in DOM"
);
const attachmentEl = document.querySelector('.o_AttachmentList .o_AttachmentCard');
assert.strictEqual(
attachmentEl.dataset.id,
messaging.models['Attachment'].findFromIdentifyingData({ id: messageAttachmentId }).localId,
"attachment component should be linked to attachment store model"
);
assert.strictEqual(
attachmentEl.title,
"test.txt",
"attachment should have filename as title attribute"
);
assert.strictEqual(
attachmentEl.querySelectorAll(`:scope .o_AttachmentCard_image`).length,
1,
"attachment should have an image part"
);
const attachmentImage = document.querySelector(`.o_AttachmentCard_image`);
assert.ok(
attachmentImage.classList.contains('o_image'),
"attachment should have o_image classname (required for mimetype.scss style)"
);
assert.strictEqual(
attachmentImage.dataset.mimetype,
'text/plain',
"attachment should have data-mimetype set (required for mimetype.scss style)"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentList_details`).length,
0,
"attachment should not have a details part"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentList_aside`).length,
0,
"attachment should not have an aside part"
);
});
QUnit.test('simplest layout + editable', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.txt",
mimetype: 'text/plain',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
async mockRPC(route, args) {
if (route.includes('web/image/750')) {
assert.ok(
route.includes('/200x200'),
"should fetch image with 200x200 pixels ratio");
assert.step('fetch_image');
}
},
});
await openDiscuss();
assert.strictEqual(
document.querySelectorAll('.o_AttachmentList').length,
1,
"should have attachment component in DOM"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentCard_image`).length,
1,
"attachment should have an image part"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentCard_details`).length,
1,
"attachment should not have a details part"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentCard_aside`).length,
1,
"attachment should have an aside part"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentCard_asideItem`).length,
2,
"attachment should have two aside item"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentCard_asideItemUnlink`).length,
1,
"attachment should have a delete button"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentCard_asideItemDownload`).length,
1,
"attachment should have a download button"
);
});
QUnit.test('link-type attachment should have open button instead of download button', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "url.example",
mimetype: 'text/plain',
type: 'url',
url: 'https://www.odoo.com',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
await contains('.o_AttachmentCard', { count: 1 });
await contains('.o_AttachmentCard_asideItemOpenLink', { count: 1 });
await contains('.o_AttachmentCard_asideItemDownload', { count: 0 });
assert.strictEqual(
document.querySelector(`.o_AttachmentCard_asideItemOpenLink`).target,
'_blank',
"attachment should have a open link button in a new tab"
);
});
QUnit.test('layout with card details and filename and extension', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.txt",
mimetype: 'text/plain',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentCard_details`).length,
1,
"attachment should have a details part"
);
assert.strictEqual(
document.querySelectorAll(`.o_AttachmentCard_extension`).length,
1,
"attachment should have its extension shown"
);
});
QUnit.test('view attachment', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.png",
mimetype: 'image/png',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { click, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_AttachmentImage img',
"attachment should have an image part"
);
await click('.o_AttachmentImage');
assert.containsOnce(
document.body,
'.o_Dialog',
'a dialog should have been opened once attachment image is clicked',
);
assert.containsOnce(
document.body,
'.o_AttachmentViewer',
'an attachment viewer should have been opened once attachment image is clicked',
);
});
QUnit.test('close attachment viewer', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.png",
mimetype: 'image/png',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { click, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_AttachmentImage img',
"attachment should have an image part"
);
await click('.o_AttachmentImage');
assert.containsOnce(
document.body,
'.o_AttachmentViewer',
"an attachment viewer should have been opened once attachment image is clicked",
);
await click('.o_AttachmentViewer_headerItemButtonClose');
assert.containsNone(
document.body,
'.o_Dialog',
"attachment viewer should be closed after clicking on close button"
);
});
QUnit.test('clicking on the delete attachment button multiple times should do the rpc only once', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.txt",
mimetype: 'text/plain',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { click, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
async mockRPC(route, args) {
if (route === '/mail/attachment/delete') {
assert.step('attachment_unlink');
}
},
});
await openDiscuss();
await click('.o_AttachmentCard_asideItemUnlink');
await afterNextRender(() => {
document.querySelector('.o_AttachmentDeleteConfirm_confirmButton').click();
document.querySelector('.o_AttachmentDeleteConfirm_confirmButton').click();
document.querySelector('.o_AttachmentDeleteConfirm_confirmButton').click();
});
assert.verifySteps(
['attachment_unlink'],
"The unlink method must be called once"
);
});
QUnit.test('[technical] does not crash when the viewer is closed before image load', async function (assert) {
/**
* When images are displayed using `src` attribute for the 1st time, it fetches the resource.
* In this case, images are actually displayed (fully fetched and rendered on screen) when
* `<image>` intercepts `load` event.
*
* Current code needs to be aware of load state of image, to display spinner when loading
* and actual image when loaded. This test asserts no crash from mishandling image becoming
* loaded from being viewed for 1st time, but viewer being closed while image is loading.
*/
assert.expect(1);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.png",
mimetype: 'image/png',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { click, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
await click('.o_AttachmentImage');
const imageEl = document.querySelector('.o_AttachmentViewer_viewImage');
await click('.o_AttachmentViewer_headerItemButtonClose');
// Simulate image becoming loaded.
let successfulLoad;
try {
imageEl.dispatchEvent(new Event('load', { bubbles: true }));
successfulLoad = true;
} catch (_err) {
successfulLoad = false;
} finally {
assert.ok(successfulLoad, 'should not crash when the image is loaded');
}
});
QUnit.test('plain text file is viewable', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.txt",
mimetype: 'text/plain',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
assert.hasClass(
document.querySelector('.o_AttachmentCard'),
'o-viewable',
"should be viewable",
);
});
QUnit.test('HTML file is viewable', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.html",
mimetype: 'text/html',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
assert.hasClass(
document.querySelector('.o_AttachmentCard'),
'o-viewable',
"should be viewable",
);
});
QUnit.test('ODT file is not viewable', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.odt",
mimetype: 'application/vnd.oasis.opendocument.text',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
assert.doesNotHaveClass(
document.querySelector('.o_AttachmentCard'),
'o-viewable',
"should not be viewable",
);
});
QUnit.test('DOCX file is not viewable', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const channelId = pyEnv['mail.channel'].create({
channel_type: 'channel',
name: 'channel1',
});
const messageAttachmentId = pyEnv['ir.attachment'].create({
name: "test.docx",
mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
pyEnv['mail.message'].create({
attachment_ids: [messageAttachmentId],
body: "<p>Test</p>",
model: 'mail.channel',
res_id: channelId
});
const { openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
assert.doesNotHaveClass(
document.querySelector('.o_AttachmentCard'),
'o-viewable',
"should not be viewable",
);
});
});
});

View file

@ -0,0 +1,77 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('call_main_view_tests.js');
QUnit.test('Join a call', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const mailChannelId = pyEnv['mail.channel'].create({});
const { click, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_callButton');
assert.containsOnce(
document.body,
'.o_CallView',
"Should have a call view"
);
assert.containsOnce(
document.body,
'.o_CallParticipantCard',
"Should have a call participant card"
);
assert.containsOnce(
document.body,
'.o_CallMainView_controls',
"Should have call controls"
);
assert.containsNone(
document.body,
'.o_ThreadViewTopbar_callButton',
"Should not have a join call button anymore"
);
});
QUnit.test('Leave a call', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId = pyEnv['mail.channel'].create({});
const { click, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_callButton');
assert.containsOnce(
document.body,
'.o_CallActionList_callToggle',
"Should have a button to leave the call"
);
await click('.o_CallActionList_callToggle');
assert.containsNone(
document.body,
'.o_CallView',
"Should not have a call view"
);
});
});
});

View file

@ -0,0 +1,207 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { browser } from '@web/core/browser/browser';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('call_settings_menu_tests.js');
QUnit.test('Renders the call settings', async function (assert) {
assert.expect(9);
patchWithCleanup(browser, {
navigator: {
...browser.navigator,
mediaDevices: {
enumerateDevices: () => Promise.resolve([
{ deviceId: 'mockAudioDeviceId', kind: 'audioinput', label: 'mockAudioDeviceLabel' },
{ deviceId: 'mockVideoDeviceId', kind: 'videoinput', label: 'mockVideoDeviceLabel' },
]),
},
}
});
const pyEnv = await startServer();
const mailChannelId = pyEnv['mail.channel'].create({});
const { click, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_openCallSettingsButton');
assert.containsOnce(
document.body,
'.o_CallSettingsMenu',
"Should have a call settings menu"
);
assert.containsN(
document.body,
'.o_CallSettingsMenu_option',
5,
"Should have five options",
);
assert.containsOnce(
document.body,
'.o_CallSettingsMenu_optionDeviceSelect',
"should have an audio device selection",
);
assert.containsOnce(
document.body,
'option[value=mockAudioDeviceId]',
"should have an option to select an audio input device",
);
assert.containsNone(
document.body,
'option[value=mockVideoDeviceId]',
"should not have an option to select a video input device",
);
assert.containsOnce(
document.body,
'.o_CallSettingsMenu_pushToTalkOption',
"should have an option to toggle push-to-talk",
);
assert.containsOnce(
document.body,
'.o_CallSettingsMenu_voiceThresholdOption',
"should have an option to set the voice detection threshold",
);
assert.containsOnce(
document.body,
'.o_CallSettingsMenu_showOnlyVideoOption',
"should have an option to filter participants who have no video",
);
assert.containsOnce(
document.body,
'.o_CallSettingsMenu_blurOption',
"should have an option to toggle the background blur feature",
);
});
QUnit.test('activate push to talk', async function (assert) {
assert.expect(3);
patchWithCleanup(browser, {
navigator: {
...browser.navigator,
mediaDevices: {
enumerateDevices: () => Promise.resolve([]),
},
}
});
const pyEnv = await startServer();
const mailChannelId = pyEnv['mail.channel'].create({});
const { click, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_openCallSettingsButton');
await click('.o_CallSettingsMenu_pushToTalkOption');
assert.containsOnce(
document.body,
'.o_CallSettingsMenu_pushToTalkKeyOption',
"should have an option set the push to talk shortcut key",
);
assert.containsOnce(
document.body,
'.o_CallSettingsMenu_pushToTalkDelayOption',
"should have an option to set the push-to-talk delay",
);
assert.containsNone(
document.body,
'.o_CallSettingsMenu_voiceThresholdOption',
"should not have an option to set the voice detection threshold",
);
});
QUnit.test('activate blur', async function (assert) {
assert.expect(2);
patchWithCleanup(browser, {
navigator: {
...browser.navigator,
mediaDevices: {
enumerateDevices: () => Promise.resolve([]),
},
}
});
const pyEnv = await startServer();
const mailChannelId = pyEnv['mail.channel'].create({});
const { click, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_openCallSettingsButton');
await click('.o_CallSettingsMenu_blurOption');
assert.containsOnce(
document.body,
'.o_CallSettingsMenu_backgroundBlurIntensityOption',
"should have an option set the background blur intensity",
);
assert.containsOnce(
document.body,
'.o_CallSettingsMenu_edgeBlurIntensityOption',
"should have an option to set the edge blur intensity",
);
});
QUnit.test("Inbox should not have any call settings menu", async (assert) => {
await startServer();
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: "mail.box_inbox",
},
},
});
await openDiscuss();
assert.containsNone($, "button[title='Show Call Settings']");
});
QUnit.test("Call settings menu should not be visible on selecting a mailbox (from being open)", async (assert) => {
patchWithCleanup(browser, {
navigator: {
...browser.navigator,
mediaDevices: {
enumerateDevices: () => Promise.resolve([]),
},
}
});
const pyEnv = await startServer();
const mailChannelId = pyEnv['mail.channel'].create({});
const { click, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await click("button[title='Show Call Settings']");
await click("button:contains(Inbox)");
assert.containsNone($, "button[title='Hide Call Settings']");
});
});
});

View file

@ -0,0 +1,160 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('channel_invitation_form_tests.js');
QUnit.test('should display the channel invitation form after clicking on the invite button of a chat', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
pyEnv['res.users'].create({ partner_id: resPartnerId1 });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'chat',
});
const { click, openDiscuss } = await start({
discuss: {
context: {
active_id: mailChannelId1,
},
},
});
await openDiscuss();
await click(`.o_ThreadViewTopbar_inviteButton`);
assert.containsOnce(
document.body,
'.o_ChannelInvitationForm',
"should display the channel invitation form after clicking on the invite button of a chat"
);
});
QUnit.test('should be able to search for a new user to invite from an existing chat', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
const resPartnerId2 = pyEnv['res.partner'].create({
email: "testpartner2@odoo.com",
name: "TestPartner2",
});
pyEnv['res.users'].create({ partner_id: resPartnerId1 });
pyEnv['res.users'].create({ partner_id: resPartnerId2 });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'chat',
});
const { click, insertText, openDiscuss } = await start({
discuss: {
context: {
active_id: mailChannelId1,
},
},
});
await openDiscuss();
await click(`.o_ThreadViewTopbar_inviteButton`);
await insertText('.o_ChannelInvitationForm_searchInput', "TestPartner2");
assert.strictEqual(
document.querySelector(`.o_ChannelInvitationFormSelectablePartner_name`).textContent,
"TestPartner2",
"should display 'TestPartner2' as it matches search term",
);
});
QUnit.test('should be able to create a new group chat from an existing chat', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
const resPartnerId2 = pyEnv['res.partner'].create({
email: "testpartner2@odoo.com",
name: "TestPartner2",
});
pyEnv['res.users'].create({ partner_id: resPartnerId1 });
pyEnv['res.users'].create({ partner_id: resPartnerId2 });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'chat',
});
const { click, insertText, openDiscuss } = await start({
discuss: {
context: {
active_id: mailChannelId1,
},
},
});
await openDiscuss();
await click(`.o_ThreadViewTopbar_inviteButton`);
await insertText('.o_ChannelInvitationForm_searchInput', "TestPartner2");
await click(`.o_ChannelInvitationFormSelectablePartner_checkbox`);
await click(`.o_ChannelInvitationForm_inviteButton`);
assert.strictEqual(
document.querySelector(`.o_ThreadViewTopbar_threadName`).textContent,
'Mitchell Admin, TestPartner, TestPartner2',
"should have created a new group chat with the existing chat members and the selected user",
);
});
QUnit.test('Invitation form should display channel group restriction', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
pyEnv['res.users'].create({ partner_id: resPartnerId1 });
const resGroupId1 = pyEnv['res.groups'].create({
name: "testGroup",
});
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
],
channel_type: 'channel',
group_public_id: resGroupId1,
});
const { click, openDiscuss } = await start({
discuss: {
context: {
active_id: mailChannelId1,
},
},
});
await openDiscuss();
await click(`.o_ThreadViewTopbar_inviteButton`);
assert.containsOnce(
document.body,
'.o_ChannelInvitationForm_accessRestrictedToGroup',
"should display the channel restriction warning"
);
});
});
});

View file

@ -0,0 +1,227 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('channel_member_list_tests.js');
QUnit.test('there should be a button to show member list in the thread view topbar initially', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'group',
});
const { openDiscuss } = await start({
discuss: {
context: {
active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_ThreadViewTopbar_showMemberListButton',
"there should be a button to show member list in the thread view topbar initially",
);
});
QUnit.test('should show member list when clicking on show member list button in thread view topbar', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'group',
});
const { click, openDiscuss } = await start({
discuss: {
context: {
active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_showMemberListButton');
assert.containsOnce(
document.body,
'.o_ChannelMemberList',
"should show member list when clicking on show member list button in thread view topbar",
);
});
QUnit.test('should have correct members in member list', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'group',
});
const { click, openDiscuss } = await start({
discuss: {
context: {
active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_showMemberListButton');
assert.containsN(
document.body,
'.o_ChannelMember',
2,
"should have 2 members in member list",
);
assert.containsOnce(
document.body,
`.o_ChannelMember[data-partner-id="${pyEnv.currentPartnerId}"]`,
"should have current partner in member list (current partner is a member)",
);
assert.containsOnce(
document.body,
`.o_ChannelMember[data-partner-id="${resPartnerId1}"]`,
"should have 'Demo' in member list ('Demo' is a member)",
);
});
QUnit.test('there should be a button to hide member list in the thread view topbar when the member list is visible', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'group',
});
const { click, openDiscuss } = await start({
discuss: {
context: {
active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_showMemberListButton');
assert.containsOnce(
document.body,
'.o_ThreadViewTopbar_hideMemberListButton',
"there should be a button to hide member list in the thread view topbar when the member list is visible",
);
});
QUnit.test('should show a button to load more members if they are not all loaded', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]];
for (let i = 0; i < 101; i++) {
const resPartnerId = pyEnv['res.partner'].create({ name: "name" + i });
channel_member_ids.push([0, 0, { partner_id: resPartnerId }]);
}
const mailChannelId = pyEnv['mail.channel'].create({
channel_member_ids,
channel_type: 'channel',
});
const { click, openDiscuss } = await start({
discuss: {
context: {
active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_showMemberListButton');
assert.containsOnce(
document.body,
'.o_ChannelMemberList_loadMoreButton',
"should have a load more button because 100 members were fetched initially and there are 102 members in total",
);
});
QUnit.test('Load more button should load more members', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]];
for (let i = 0; i < 101; i++) {
const resPartnerId = pyEnv['res.partner'].create({ name: "name" + i });
channel_member_ids.push([0, 0, { partner_id: resPartnerId }]);
}
const mailChannelId = pyEnv['mail.channel'].create({
channel_member_ids,
channel_type: 'channel',
});
const { click, openDiscuss } = await start({
discuss: {
context: {
active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_showMemberListButton');
await click('.o_ChannelMemberList_loadMoreButton');
assert.containsN(
document.body,
'.o_ChannelMember',
102,
"should load all the members because 100 members were fetched initially, and load more fetched the remaining 2 members",
);
});
QUnit.test('chat with member should be opened after clicking on channel member', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" });
pyEnv['res.users'].create({ partner_id: resPartnerId1 });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'channel',
});
const { click, openDiscuss } = await start({
discuss: {
context: {
active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
await click('.o_ThreadViewTopbar_showMemberListButton');
await click(`.o_ChannelMember[data-partner-id="${resPartnerId1}"]`);
assert.containsOnce(
document.body,
`.o_ThreadView[data-correspondent-id="${resPartnerId1}"]`,
"Chat with member Demo should be opened",
);
});
});
});

View file

@ -0,0 +1,69 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('channel_preview_view_tests.js');
QUnit.test('mark as read', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, {
message_unread_counter: 1, // mandatory for good working of test, but ideally should be deduced by other server data
partner_id: pyEnv.currentPartnerId,
}],
[0, 0, {
partner_id: resPartnerId1,
}],
],
});
const [mailMessageId1] = pyEnv['mail.message'].create([
{ author_id: resPartnerId1, model: 'mail.channel', res_id: mailChannelId1 },
{ author_id: resPartnerId1, model: 'mail.channel', res_id: mailChannelId1 },
]);
const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]);
pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 });
const { click } = await start({
async mockRPC(route, args) {
if (route.includes('set_last_seen_message')) {
assert.step('set_last_seen_message');
}
},
});
await click('.o_MessagingMenu_toggler');
assert.containsOnce(
document.body,
'.o_ChannelPreviewView_markAsRead',
"should have the mark as read button"
);
await click('.o_ChannelPreviewView_markAsRead');
assert.verifySteps(
['set_last_seen_message'],
"should have marked the thread as seen"
);
assert.hasClass(
document.querySelector('.o_ChannelPreviewView'),
'o-muted',
"should be muted once marked as read"
);
assert.containsNone(
document.body,
'.o_ChannelPreviewView_markAsRead',
"should no longer have the mark as read button"
);
assert.containsNone(
document.body,
'.o_ChatWindow',
"should not have opened the thread"
);
});
});
});

View file

@ -0,0 +1,303 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('chatter_suggested_recipients_tests.js');
QUnit.test("suggest recipient on 'Send message' composer", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" });
const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be", partner_ids: [resPartnerId1] });
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
assert.containsOnce(
document.body,
'.o_ComposerSuggestedRecipientList',
"Should display a list of suggested recipients after opening the composer from 'Send message' button"
);
});
QUnit.test("with 3 or less suggested recipients: no 'show more' button", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" });
const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be", partner_ids: [resPartnerId1] });
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
assert.containsNone(
document.body,
'.o_ComposerSuggestedRecipientList_showMore',
"should not display 'show more' button with 3 or less suggested recipients"
);
});
QUnit.test("display reason for suggested recipient on mouse over", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" });
const resFakeId1 = pyEnv['res.fake'].create({ partner_ids: [resPartnerId1] });
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
const partnerTitle = document.querySelector(`.o_ComposerSuggestedRecipient[data-partner-id="${resPartnerId1}"]`).getAttribute('title');
assert.strictEqual(
partnerTitle,
"Add as recipient and follower (reason: Email partner)",
"must display reason for suggested recipient on mouse over",
);
});
QUnit.test("suggested recipient without partner are unchecked by default", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be" });
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
const checkboxUnchecked = document.querySelector('.o_ComposerSuggestedRecipient:not([data-partner-id]) input[type=checkbox]');
assert.notOk(
checkboxUnchecked.checked,
"suggested recipient without partner must be unchecked by default",
);
});
QUnit.test("suggested recipient without partner are unchecked when closing the dialog without creating partner", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be" });
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
// click on checkbox to open dialog
await document.querySelector('.o_ComposerSuggestedRecipient:not([data-partner-id]) input[type=checkbox]').click();
function waitForElm(selector) {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
await waitForElm('.modal-header');
// close dialog without changing anything
document.querySelector('.modal-header > button.btn-close').click();
assert.notOk(
document.querySelector('.o_ComposerSuggestedRecipient:not([data-partner-id]) input[type=checkbox]').checked,
"suggested recipient without partner must be unchecked",
);
});
QUnit.test("suggested recipient with partner are checked by default", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" });
const resFakeId1 = pyEnv['res.fake'].create({ partner_ids: [resPartnerId1] });
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
const checkboxChecked = document.querySelector(`.o_ComposerSuggestedRecipient[data-partner-id="${resPartnerId1}"] input[type=checkbox]`);
assert.ok(
checkboxChecked.checked,
"suggested recipient with partner must be checked by default",
);
});
QUnit.test("more than 3 suggested recipients: display only 3 and 'show more' button", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([
{ display_name: "John Jane", email: "john@jane.be" },
{ display_name: "Jack Jone", email: "jack@jone.be" },
{ display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" },
{ display_name: "jolly Roger", email: "Roger@skullflag.com" },
]);
const resFakeId1 = pyEnv['res.fake'].create({
partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4],
});
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
assert.containsOnce(
document.body,
'.o_ComposerSuggestedRecipientList_showMore',
"more than 3 suggested recipients display 'show more' button"
);
});
QUnit.test("more than 3 suggested recipients: show all of them on click 'show more' button", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([
{ display_name: "John Jane", email: "john@jane.be" },
{ display_name: "Jack Jone", email: "jack@jone.be" },
{ display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" },
{ display_name: "jolly Roger", email: "Roger@skullflag.com" },
]);
const resFakeId1 = pyEnv['res.fake'].create({
partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4],
});
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
await click(`.o_ComposerSuggestedRecipientList_showMore`);
assert.containsN(
document.body,
'.o_ComposerSuggestedRecipient',
4,
"more than 3 suggested recipients: show all of them on click 'show more' button"
);
});
QUnit.test("more than 3 suggested recipients -> click 'show more' -> 'show less' button", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([
{ display_name: "John Jane", email: "john@jane.be" },
{ display_name: "Jack Jone", email: "jack@jone.be" },
{ display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" },
{ display_name: "jolly Roger", email: "Roger@skullflag.com" },
]);
const resFakeId1 = pyEnv['res.fake'].create({
partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4],
});
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
await click(`.o_ComposerSuggestedRecipientList_showMore`);
assert.containsOnce(
document.body,
'.o_ComposerSuggestedRecipientList_showLess',
"more than 3 suggested recipients -> click 'show more' -> 'show less' button"
);
});
QUnit.test("suggested recipients list display 3 suggested recipient and 'show more' button when 'show less' button is clicked", async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([
{ display_name: "John Jane", email: "john@jane.be" },
{ display_name: "Jack Jone", email: "jack@jone.be" },
{ display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" },
{ display_name: "jolly Roger", email: "Roger@skullflag.com" },
]);
const resFakeId1 = pyEnv['res.fake'].create({
partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4],
});
const { click, openView } = await start();
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonSendMessage`);
await click(`.o_ComposerSuggestedRecipientList_showMore`);
await click(`.o_ComposerSuggestedRecipientList_showLess`);
assert.containsN(
document.body,
'.o_ComposerSuggestedRecipient',
3,
"suggested recipient list should display 3 suggested recipients after clicking on 'show less'."
);
assert.containsOnce(
document.body,
'.o_ComposerSuggestedRecipientList_showMore',
"suggested recipient list should containt a 'show More' button after clicking on 'show less'."
);
});
QUnit.test("suggested recipients should not be notified when posting an internal note", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" });
const resFakeId1 = pyEnv['res.fake'].create({ partner_ids: [resPartnerId1] });
const { click, insertText, openView } = await start({
async mockRPC(route, args) {
if (route === '/mail/message/post') {
assert.strictEqual(
args.post_data.partner_ids.length,
0,
"post data should not contain suggested recipients when posting an internal note"
);
}
},
});
await openView({
res_id: resFakeId1,
res_model: 'res.fake',
views: [[false, 'form']],
});
await click(`.o_ChatterTopbar_buttonLogNote`);
await insertText('.o_ComposerTextInput_textarea', "Dummy Message");
await click('.o_Composer_buttonSend');
});
});
});

View file

@ -0,0 +1,537 @@
/** @odoo-module **/
import {
afterNextRender,
nextAnimationFrame,
start,
startServer
} from '@mail/../tests/helpers/test_utils';
import { contains, createFile, dragenterFiles, dropFiles } from "@web/../tests/utils";
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('chatter', {}, function () {
QUnit.module('chatter_tests.js');
QUnit.test('base rendering when chatter has no attachment', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
for (let i = 0; i < 60; i++) {
pyEnv['mail.message'].create({
body: "not empty",
model: 'res.partner',
res_id: resPartnerId1,
});
}
const { openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_Chatter`).length,
1,
"should have a chatter"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_attachmentBox`).length,
0,
"should not have an attachment box in the chatter"
);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_thread`).length,
1,
"should have a thread in the chatter"
);
assert.containsOnce(
document.body,
`.o_Chatter_thread[data-thread-id="${resPartnerId1}"][data-thread-model="res.partner"]`,
"chatter should have the right thread."
);
assert.strictEqual(
document.querySelectorAll(`.o_Message`).length,
30,
"the first 30 messages of thread should be loaded"
);
});
QUnit.test('base rendering when chatter has no record', async function (assert) {
assert.expect(9);
const { click, openView } = await start();
await openView({
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_Chatter`).length,
1,
"should have a chatter"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_attachmentBox`).length,
0,
"should not have an attachment box in the chatter"
);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_thread`).length,
1,
"should have a thread in the chatter"
);
assert.strictEqual(
document.querySelectorAll(`.o_Message`).length,
1,
"should have a message"
);
assert.strictEqual(
document.querySelector(`.o_Message_content`).textContent,
"Creating a new record...",
"should have the 'Creating a new record ...' message"
);
assert.containsNone(
document.body,
'.o_MessageList_loadMore',
"should not have the 'load more' button"
);
await click('.o_Message');
assert.strictEqual(
document.querySelectorAll(`.o_MessageActionList`).length,
1,
"should action list in message"
);
assert.containsNone(
document.body,
'.o_MessageActionView',
"should not have any action in action list of message"
);
});
QUnit.test('base rendering when chatter has attachments', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['ir.attachment'].create([
{
mimetype: 'text/plain',
name: 'Blah.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
{
mimetype: 'text/plain',
name: 'Blu.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
]);
const { openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_Chatter`).length,
1,
"should have a chatter"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_attachmentBox`).length,
0,
"should not have an attachment box in the chatter"
);
});
QUnit.test('show attachment box', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['ir.attachment'].create([
{
mimetype: 'text/plain',
name: 'Blah.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
{
mimetype: 'text/plain',
name: 'Blu.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
]);
const { click, openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_Chatter`).length,
1,
"should have a chatter"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length,
1,
"should have an attachments button in chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length,
1,
"attachments button should have a counter"
);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_attachmentBox`).length,
0,
"should not have an attachment box in the chatter"
);
await click(`.o_ChatterTopbar_buttonToggleAttachments`);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_attachmentBox`).length,
1,
"should have an attachment box in the chatter"
);
});
QUnit.test("chatter: drop attachments", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({});
const { openView } = await start();
openView({
res_id: partnerId,
res_model: "res.partner",
views: [[false, "form"]],
});
const files = [
await createFile({
content: "hello, world",
contentType: "text/plain",
name: "text.txt",
}),
await createFile({
content: "hello, worlduh",
contentType: "text/plain",
name: "text2.txt",
}),
];
await dragenterFiles(".o_Chatter", files);
await contains(".o_Chatter_dropZone");
await contains(".o_AttachmentCard", { count: 0 });
await dropFiles(".o_Chatter_dropZone", files);
await contains(".o_AttachmentCard", { count: 2 });
const extraFiles = [
await createFile({
content: "hello, world",
contentType: "text/plain",
name: "text3.txt",
}),
];
await dragenterFiles(".o_Chatter", extraFiles);
await dropFiles(".o_Chatter_dropZone", extraFiles);
await contains(".o_AttachmentCard", { count: 3 });
});
QUnit.test("error on uploading file in chatter shows error", async function (assert) {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({});
const { openFormView } = await start({
async mockRPC(route) {
if (route === '/mail/attachment/upload') {
throw new Error("cannot upload this file");
}
}
});
await openFormView({
res_id: partnerId,
res_model: "res.partner",
});
const files = [
await createFile({
content: "hello, world",
contentType: "text/plain",
name: "text.txt",
}),
];
await dragenterFiles(".o_Chatter", files);
await contains(".o_Chatter_dropZone");
await contains(".o_AttachmentCard", { count: 0 });
await dropFiles(".o_Chatter_dropZone", files);
await contains(".o_notification", { text: "cannot upload this file" });
});
QUnit.test('composer show/hide on log note/send message [REQUIRE FOCUS]', async function (assert) {
assert.expect(10);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length,
1,
"should have a send message button"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length,
1,
"should have a log note button"
);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_composer`).length,
0,
"should not have a composer"
);
await click(`.o_ChatterTopbar_buttonSendMessage`);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_composer`).length,
1,
"should have a composer"
);
assert.hasClass(
document.querySelector('.o_Chatter_composer'),
'o-focused',
"composer 'send message' in chatter should have focus just after being displayed"
);
await click(`.o_ChatterTopbar_buttonLogNote`);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_composer`).length,
1,
"should still have a composer"
);
assert.hasClass(
document.querySelector('.o_Chatter_composer'),
'o-focused',
"composer 'log note' in chatter should have focus just after being displayed"
);
await click(`.o_ChatterTopbar_buttonLogNote`);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_composer`).length,
0,
"should have no composer anymore"
);
await click(`.o_ChatterTopbar_buttonSendMessage`);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_composer`).length,
1,
"should have a composer"
);
await click(`.o_ChatterTopbar_buttonSendMessage`);
assert.strictEqual(
document.querySelectorAll(`.o_Chatter_composer`).length,
0,
"should have no composer anymore"
);
});
QUnit.test('should display subject when subject is not the same as the thread name', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['mail.message'].create({
body: "not empty",
model: 'res.partner',
res_id: resPartnerId1,
subject: "Salutations, voyageur",
});
const { openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_Message_subject',
"should display subject of the message"
);
assert.strictEqual(
document.querySelector('.o_Message_subject').textContent,
"Subject: Salutations, voyageur",
"Subject of the message should be 'Salutations, voyageur'"
);
});
QUnit.test('should not display subject when subject is the same as the thread name', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Salutations, voyageur" });
pyEnv['mail.message'].create({
body: "not empty",
model: 'res.partner',
res_id: resPartnerId1,
subject: "Salutations, voyageur",
});
const { openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsNone(
document.body,
'.o_Message_subject',
"should not display subject of the message"
);
});
QUnit.test('should not display user notification messages in chatter', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['mail.message'].create({
message_type: 'user_notification',
model: 'res.partner',
res_id: resPartnerId1,
});
const { openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsNone(
document.body,
'.o_Message',
"should display no messages"
);
});
QUnit.test('post message with "CTRL-Enter" keyboard shortcut', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, insertText, openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsNone(
document.body,
'.o_Message',
"should not have any message initially in chatter"
);
await click('.o_ChatterTopbar_buttonSendMessage');
await insertText('.o_ComposerTextInput_textarea', "Test");
await afterNextRender(() => {
const kevt = new window.KeyboardEvent('keydown', { ctrlKey: true, key: "Enter" });
document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(kevt);
});
assert.containsOnce(
document.body,
'.o_Message',
"should now have single message in chatter after posting message from pressing 'CTRL-Enter' in text input of composer"
);
});
QUnit.test('post message with "META-Enter" keyboard shortcut', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, insertText, openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsNone(
document.body,
'.o_Message',
"should not have any message initially in chatter"
);
await click('.o_ChatterTopbar_buttonSendMessage');
await insertText('.o_ComposerTextInput_textarea', "Test");
await afterNextRender(() => {
const kevt = new window.KeyboardEvent('keydown', { key: "Enter", metaKey: true });
document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(kevt);
});
assert.containsOnce(
document.body,
'.o_Message',
"should now have single message in channel after posting message from pressing 'META-Enter' in text input of composer"
);
});
QUnit.test('do not post message with "Enter" keyboard shortcut', async function (assert) {
// Note that test doesn't assert Enter makes a newline, because this
// default browser cannot be simulated with just dispatching
// programmatically crafted events...
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, insertText, openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsNone(
document.body,
'.o_Message',
"should not have any message initially in chatter"
);
await click('.o_ChatterTopbar_buttonSendMessage');
await insertText('.o_ComposerTextInput_textarea', "Test");
const kevt = new window.KeyboardEvent('keydown', { key: "Enter" });
document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(kevt);
await nextAnimationFrame();
assert.containsNone(
document.body,
'.o_Message',
"should still not have any message in mailing channel after pressing 'Enter' in text input of composer"
);
});
});
});
});

View file

@ -0,0 +1,531 @@
/** @odoo-module **/
import { afterNextRender, nextAnimationFrame, start, startServer } from '@mail/../tests/helpers/test_utils';
import { makeTestPromise } from 'web.test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('chatter_topbar_tests.js');
QUnit.test('base rendering', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length,
1,
"should have a send message button in chatter menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length,
1,
"should have a log note button in chatter menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonScheduleActivity`).length,
1,
"should have a schedule activity button in chatter menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length,
1,
"should have an attachments button in chatter menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length,
0,
"attachments button should not have a loader"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_followerListMenu`).length,
1,
"should have a follower menu"
);
});
QUnit.test('attachment loading is delayed', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { advanceTime, openView } = await start({
hasTimeControl: true,
loadingBaseDelayDuration: 100,
async mockRPC(route) {
if (route.includes('/mail/thread/data')) {
await makeTestPromise(); // simulate long loading
}
},
});
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length,
1,
"should have an attachments button in chatter menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length,
0,
"attachments button should not have a loader yet"
);
await afterNextRender(async () => advanceTime(100));
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length,
1,
"attachments button should now have a loader"
);
});
QUnit.test('attachment counter while loading attachments', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { openView } = await start({
async mockRPC(route) {
if (route.includes('/mail/thread/data')) {
await makeTestPromise(); // simulate long loading
}
}
});
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length,
1,
"should have an attachments button in chatter menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length,
1,
"attachments button should have a loader"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length,
0,
"attachments button should not have a counter"
);
});
QUnit.test('attachment counter transition when attachments become loaded)', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const attachmentPromise = makeTestPromise();
const { openView } = await start({
async mockRPC(route) {
if (route.includes('/mail/thread/data')) {
await attachmentPromise;
}
},
});
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length,
1,
"should have an attachments button in chatter menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length,
1,
"attachments button should have a loader"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length,
0,
"attachments button should not have a counter"
);
await afterNextRender(() => attachmentPromise.resolve());
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length,
1,
"should have an attachments button in chatter menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length,
0,
"attachments button should not have a loader"
);
});
QUnit.test('attachment counter without attachments', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length,
1,
"should have an attachments button in chatter menu"
);
});
QUnit.test('attachment counter with attachments', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['ir.attachment'].create([
{
mimetype: 'text/plain',
name: 'Blah.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
{
mimetype: 'text/plain',
name: 'Blu.txt',
res_id: resPartnerId1,
res_model: 'res.partner',
},
]);
const { openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar`).length,
1,
"should have a chatter topbar"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length,
1,
"should have an attachments button in chatter menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length,
1,
"attachments button should have a counter"
);
assert.strictEqual(
document.querySelector(`.o_ChatterTopbar_buttonAttachmentsCount`).textContent,
'2',
'attachment counter content should contain "2 files"'
);
});
QUnit.test('composer state conserved when clicking on another topbar button', async function (assert) {
assert.expect(8);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
`.o_ChatterTopbar`,
"should have a chatter topbar"
);
assert.containsOnce(
document.body,
`.o_ChatterTopbar_buttonSendMessage`,
"should have a send message button in chatter menu"
);
assert.containsOnce(
document.body,
`.o_ChatterTopbar_buttonLogNote`,
"should have a log note button in chatter menu"
);
assert.containsOnce(
document.body,
`.o_ChatterTopbar_buttonAddAttachments`,
"should have an attachments button in chatter menu"
);
await click(`.o_ChatterTopbar_buttonLogNote`);
assert.containsOnce(
document.body,
`.o_ChatterTopbar_buttonLogNote.o-active`,
"log button should now be active"
);
assert.containsNone(
document.body,
`.o_ChatterTopbar_buttonSendMessage.o-active`,
"send message button should not be active"
);
document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).click();
await nextAnimationFrame();
assert.containsOnce(
document.body,
`.o_ChatterTopbar_buttonLogNote.o-active`,
"log button should still be active"
);
assert.containsNone(
document.body,
`.o_ChatterTopbar_buttonSendMessage.o-active`,
"send message button should still be not active"
);
});
QUnit.test('rendering with multiple partner followers', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([
{ name: 'resPartner1' },
{ name: 'resPartner2' },
{ message_follower_ids: [1, 2] },
]);
pyEnv['mail.followers'].create([
{
name: "Jean Michang",
partner_id: resPartnerId2,
res_id: resPartnerId3,
res_model: 'res.partner',
},
{
name: "Eden Hazard",
partner_id: resPartnerId1,
res_id: resPartnerId3,
res_model: 'res.partner',
},
]);
const { click, openView } = await start();
await openView({
res_id: resPartnerId3,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_FollowerListMenu',
"should have followers menu component"
);
assert.containsOnce(
document.body,
'.o_FollowerListMenu_buttonFollowers',
"should have followers button"
);
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_FollowerListMenu_dropdown',
"followers dropdown should be opened"
);
assert.containsN(
document.body,
'.o_Follower',
2,
"exactly two followers should be listed"
);
assert.containsN(
document.body,
'.o_Follower_name',
2,
"exactly two follower names should be listed"
);
assert.strictEqual(
document.querySelectorAll('.o_Follower_name')[0].textContent.trim(),
"Jean Michang",
"first follower is 'Jean Michang'"
);
assert.strictEqual(
document.querySelectorAll('.o_Follower_name')[1].textContent.trim(),
"Eden Hazard",
"second follower is 'Eden Hazard'"
);
});
QUnit.test('log note/send message switching', async function (assert) {
assert.expect(8);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_ChatterTopbar_buttonSendMessage',
"should have a 'Send Message' button"
);
assert.doesNotHaveClass(
document.querySelector('.o_ChatterTopbar_buttonSendMessage'),
'o-active',
"'Send Message' button should not be active"
);
assert.containsOnce(
document.body,
'.o_ChatterTopbar_buttonLogNote',
"should have a 'Log Note' button"
);
assert.doesNotHaveClass(
document.querySelector('.o_ChatterTopbar_buttonLogNote'),
'o-active',
"'Log Note' button should not be active"
);
await click(`.o_ChatterTopbar_buttonSendMessage`);
assert.hasClass(
document.querySelector('.o_ChatterTopbar_buttonSendMessage'),
'o-active',
"'Send Message' button should be active"
);
assert.doesNotHaveClass(
document.querySelector('.o_ChatterTopbar_buttonLogNote'),
'o-active',
"'Log Note' button should not be active"
);
await click(`.o_ChatterTopbar_buttonLogNote`);
assert.doesNotHaveClass(
document.querySelector('.o_ChatterTopbar_buttonSendMessage'),
'o-active',
"'Send Message' button should not be active"
);
assert.hasClass(
document.querySelector('.o_ChatterTopbar_buttonLogNote'),
'o-active',
"'Log Note' button should be active"
);
});
QUnit.test('log note toggling', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_ChatterTopbar_buttonLogNote',
"should have a 'Log Note' button"
);
assert.doesNotHaveClass(
document.querySelector('.o_ChatterTopbar_buttonLogNote'),
'o-active',
"'Log Note' button should not be active"
);
await click(`.o_ChatterTopbar_buttonLogNote`);
assert.hasClass(
document.querySelector('.o_ChatterTopbar_buttonLogNote'),
'o-active',
"'Log Note' button should be active"
);
await click(`.o_ChatterTopbar_buttonLogNote`);
assert.doesNotHaveClass(
document.querySelector('.o_ChatterTopbar_buttonLogNote'),
'o-active',
"'Log Note' button should not be active"
);
});
QUnit.test('send message toggling', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, openView } = await start();
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_ChatterTopbar_buttonSendMessage',
"should have a 'Send Message' button"
);
assert.doesNotHaveClass(
document.querySelector('.o_ChatterTopbar_buttonSendMessage'),
'o-active',
"'Send Message' button should not be active"
);
await click(`.o_ChatterTopbar_buttonSendMessage`);
assert.hasClass(
document.querySelector('.o_ChatterTopbar_buttonSendMessage'),
'o-active',
"'Send Message' button should be active"
);
await click(`.o_ChatterTopbar_buttonSendMessage`);
assert.doesNotHaveClass(
document.querySelector('.o_ChatterTopbar_buttonSendMessage'),
'o-active',
"'Send Message' button should not be active"
);
});
});
});

View file

@ -0,0 +1,100 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('composer_suggestion_canned_response_tests.js');
QUnit.test('canned response suggestion displayed', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
pyEnv['mail.shortcode'].create({
source: 'hello',
substitution: "Hello, how are you?",
});
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', ":hello");
assert.containsOnce(
document.body,
`.o_ComposerSuggestionView`,
"Canned response suggestion should be present"
);
});
QUnit.test('canned response suggestion correct data', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
pyEnv['mail.shortcode'].create({
source: 'hello',
substitution: "Hello, how are you?",
});
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', ":hello");
assert.containsOnce(
document.body,
'.o_ComposerSuggestionView_part1',
"Canned response source should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestionView_part1`).textContent,
"hello",
"Canned response source should be displayed"
);
assert.containsOnce(
document.body,
'.o_ComposerSuggestionView_part2',
"Canned response substitution should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestionView_part2`).textContent,
"Hello, how are you?",
"Canned response substitution should be displayed"
);
});
QUnit.test('canned response suggestion active', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
pyEnv['mail.shortcode'].create({
source: 'hello',
substitution: "Hello, how are you?",
});
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', ":hello");
assert.hasClass(
document.querySelector('.o_ComposerSuggestionView'),
'active',
"should be active initially"
);
});
});
});

View file

@ -0,0 +1,78 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('composer_suggestion_channel_tests.js');
QUnit.test('channel mention suggestion displayed', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' });
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "#my-channel");
assert.containsOnce(
document.body,
`.o_ComposerSuggestionView`,
"Channel mention suggestion should be present"
);
});
QUnit.test('channel mention suggestion correct data', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" });
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "#General");
assert.containsOnce(
document.body,
'.o_ComposerSuggestionView_part1',
"Channel name should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestionView_part1`).textContent,
"General",
"Channel name should be displayed"
);
});
QUnit.test('channel mention suggestion active', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' });
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "#my-channel");
assert.hasClass(
document.querySelector('.o_ComposerSuggestionView'),
'active',
"should be active initially"
);
});
});
});

View file

@ -0,0 +1,88 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('composer_suggestion_command_tests.js');
QUnit.test('command suggestion displayed', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' });
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "/who");
assert.containsOnce(
document.body,
'.o_ComposerSuggestionView',
"Command suggestion should be present",
);
});
QUnit.test('command suggestion correct data', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' });
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "/who");
assert.containsOnce(
document.body,
'.o_ComposerSuggestionView_part1',
"Command name should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestionView_part1`).textContent,
"who",
"Command name should be displayed"
);
assert.containsOnce(
document.querySelector('.o_ComposerSuggestionView'),
'.o_ComposerSuggestionView_part2',
"Command help should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestionView_part2`).textContent,
"List users in the current channel",
"Command help should be displayed"
);
});
QUnit.test('command suggestion active', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' });
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "/who");
assert.hasClass(
document.querySelector('.o_ComposerSuggestionView'),
'active',
"1st suggestion should be active initially"
);
});
});
});

View file

@ -0,0 +1,123 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('composer_suggestion_partner_tests.js');
QUnit.test('partner mention suggestion displayed', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId = pyEnv['res.partner'].create({
email: "demo_user@odoo.com",
im_status: 'online',
name: 'Demo User',
});
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId }],
],
});
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "@demo");
assert.containsOnce(
document.body,
`.o_ComposerSuggestionView`,
"Partner mention suggestion should be present"
);
});
QUnit.test('partner mention suggestion correct data', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const resPartnerId = pyEnv['res.partner'].create({
email: "demo_user@odoo.com",
im_status: 'online',
name: 'Demo User',
});
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId }],
],
});
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "@demo");
assert.containsOnce(
document.querySelector('.o_ComposerSuggestionView'),
'.o_PersonaImStatusIcon',
"Partner's im_status should be displayed"
);
assert.containsOnce(
document.body,
'.o_ComposerSuggestionView_part1',
"Partner's name should be present"
);
assert.strictEqual(
document.querySelector('.o_ComposerSuggestionView_part1').textContent,
"Demo User",
"Partner's name should be displayed"
);
assert.containsOnce(
document.body,
'.o_ComposerSuggestionView_part2',
"Partner's email should be present"
);
assert.strictEqual(
document.querySelector('.o_ComposerSuggestionView_part2').textContent,
"(demo_user@odoo.com)",
"Partner's email should be displayed"
);
});
QUnit.test('partner mention suggestion active', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId = pyEnv['res.partner'].create({
email: "demo_user@odoo.com",
im_status: 'online',
name: 'Demo User',
});
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId }],
],
});
const { insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId1,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "@demo");
assert.hasClass(
document.querySelector('.o_ComposerSuggestionView'),
'active',
"should be active initially"
);
});
});
});

View file

@ -0,0 +1,49 @@
/** @odoo-module **/
import { makeDeferred } from '@mail/utils/deferred';
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('dialog_manager_tests.js');
QUnit.test('[technical] messaging not created', async function (assert) {
/**
* Creation of messaging in env is async due to generation of models being
* async. Generation of models is async because it requires parsing of all
* JS modules that contain pieces of model definitions.
*
* Time of having no messaging is very short, almost imperceptible by user
* on UI, but the display should not crash during this critical time period.
*/
assert.expect(1);
const messagingBeforeCreationDeferred = makeDeferred();
const { afterNextRender } = await start({
messagingBeforeCreationDeferred,
waitUntilMessagingCondition: 'none',
});
// simulate messaging being created
await afterNextRender(messagingBeforeCreationDeferred.resolve);
assert.containsOnce(
document.body,
'.o_DialogManager',
"should contain dialog manager after messaging has been created"
);
});
QUnit.test('initial mount', async function (assert) {
assert.expect(1);
await start();
assert.containsOnce(
document.body,
'.o_DialogManager',
"should have dialog manager"
);
});
});
});

View file

@ -0,0 +1,911 @@
/** @odoo-module **/
import {
afterNextRender,
nextAnimationFrame,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_inbox_tests.js');
QUnit.test('reply: discard on pressing escape', async function (assert) {
assert.expect(9);
const pyEnv = await startServer();
// partner expected to be found by mention
pyEnv['res.partner'].create({
email: "testpartnert@odoo.com",
name: "TestPartner",
});
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: 20,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, insertText, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await click('.o_Message');
await click('.o_MessageActionView_actionReplyTo');
assert.containsOnce(
document.body,
'.o_Composer',
"should have composer after clicking on reply to message"
);
await click(`.o_Composer_buttonEmojis`);
assert.containsOnce(
document.body,
'.o_EmojiPickerView',
"emoji list should be opened after click on emojis button"
);
await afterNextRender(() => {
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_Composer_buttonEmojis`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_EmojiPickerView',
"emoji list should be closed after pressing escape on emojis button"
);
assert.containsOnce(
document.body,
'.o_Composer',
"reply composer should still be opened after pressing escape on emojis button"
);
await insertText('.o_ComposerTextInput_textarea', "@Te");
assert.containsOnce(
document.body,
'.o_ComposerSuggestionView',
"mention suggestion should be opened after typing @"
);
await afterNextRender(() => {
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_ComposerSuggestionView',
"mention suggestion should be closed after pressing escape on mention suggestion"
);
assert.containsOnce(
document.body,
'.o_Composer',
"reply composer should still be opened after pressing escape on mention suggestion"
);
await afterNextRender(() => {
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_Composer',
"reply composer should be closed after pressing escape if there was no other priority escape handler"
);
});
QUnit.test('reply: discard on discard button click', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await click('.o_Message');
await click('.o_MessageActionView_actionReplyTo');
assert.containsOnce(
document.body,
'.o_Composer',
"should have composer after clicking on reply to message"
);
assert.containsOnce(
document.body,
'.o_Composer_buttonDiscard',
"composer should have a discard button"
);
await click(`.o_Composer_buttonDiscard`);
assert.containsNone(
document.body,
'.o_Composer',
"reply composer should be closed after clicking on discard"
);
});
QUnit.test('reply: discard on reply button toggle', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await click('.o_Message');
await click('.o_MessageActionView_actionReplyTo');
assert.containsOnce(
document.body,
'.o_Composer',
"should have composer after clicking on reply to message"
);
await click(`.o_MessageActionView_actionReplyTo`);
assert.containsNone(
document.body,
'.o_Composer',
"reply composer should be closed after clicking on reply button again"
);
});
QUnit.test('reply: discard on click away', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await click('.o_Message');
await click('.o_MessageActionView_actionReplyTo');
assert.containsOnce(
document.body,
'.o_Composer',
"should have composer after clicking on reply to message"
);
document.querySelector(`.o_ComposerTextInput_textarea`).click();
await nextAnimationFrame(); // wait just in case, but nothing is supposed to happen
assert.containsOnce(
document.body,
'.o_Composer',
"reply composer should still be there after clicking inside itself"
);
await click(`.o_Composer_buttonEmojis`);
assert.containsOnce(
document.body,
'.o_EmojiPickerView',
"emoji list should be opened after clicking on emojis button"
);
await click(`.o_EmojiView`);
assert.containsNone(
document.body,
'.o_EmojiPickerView',
"emoji list should be closed after selecting an emoji"
);
assert.containsOnce(
document.body,
'.o_Composer',
"reply composer should still be there after selecting an emoji (even though it is technically a click away, it should be considered inside)"
);
await click(`.o_Message`);
assert.containsNone(
document.body,
'.o_Composer',
"reply composer should be closed after clicking away"
);
});
QUnit.test('"reply to" composer should log note if message replied to is a note', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
is_discussion: false,
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, insertText, messaging, openDiscuss } = await start({
async mockRPC(route, args) {
if (route === '/mail/message/post') {
assert.step('/mail/message/post');
assert.strictEqual(
args.post_data.message_type,
"comment",
"should set message type as 'comment'"
);
assert.strictEqual(
args.post_data.subtype_xmlid,
"mail.mt_note",
"should set subtype_xmlid as 'note'"
);
}
},
});
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await click('.o_Message');
await click('.o_MessageActionView_actionReplyTo');
assert.strictEqual(
document.querySelector('.o_Composer_buttonSend').textContent.trim(),
"Log",
"Send button text should be 'Log'"
);
await insertText('.o_ComposerTextInput_textarea', "Test");
await click('.o_Composer_buttonSend');
assert.verifySteps(['/mail/message/post']);
});
QUnit.test('"reply to" composer should send message if message replied to is not a note', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
is_discussion: true,
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, insertText, messaging, openDiscuss } = await start({
async mockRPC(route, args) {
if (route === '/mail/message/post') {
assert.step('/mail/message/post');
assert.strictEqual(
args.post_data.message_type,
"comment",
"should set message type as 'comment'"
);
assert.strictEqual(
args.post_data.subtype_xmlid,
"mail.mt_comment",
"should set subtype_xmlid as 'comment'"
);
}
},
});
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await click('.o_Message');
await click('.o_MessageActionView_actionReplyTo');
assert.strictEqual(
document.querySelector('.o_Composer_buttonSend').textContent.trim(),
"Send",
"Send button text should be 'Send'"
);
await insertText('.o_ComposerTextInput_textarea', "Test");
await click('.o_Composer_buttonSend');
assert.verifySteps(['/mail/message/post']);
});
QUnit.test('error notifications should not be shown in Inbox', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'mail.channel',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1, // id of related message
notification_status: 'exception',
notification_type: 'email',
res_partner_id: pyEnv.currentPartnerId, // must be for current partner
});
const { openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
assert.containsOnce(
document.body,
'.o_Message_originThreadLink',
"should display origin thread link"
);
assert.containsNone(
document.body,
'.o_Message_notificationIcon',
"should not display any notification icon in Inbox"
);
});
QUnit.test('show subject of message in Inbox', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'mail.channel',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId], // not needed, for consistency
subject: "Salutations, voyageur",
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
assert.containsOnce(
document.body,
'.o_Message_subject',
"should display subject of the message"
);
assert.strictEqual(
document.querySelector('.o_Message_subject').textContent,
"Subject: Salutations, voyageur",
"Subject of the message should be 'Salutations, voyageur'"
);
});
QUnit.test('show subject of message in history', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
history_partner_ids: [3], // not needed, for consistency
model: 'mail.channel',
subject: "Salutations, voyageur",
});
pyEnv['mail.notification'].create({
is_read: true,
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging, openDiscuss } = await start({
discuss: {
params: {
default_active_id: 'mail.box_history',
},
},
});
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until history displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.history.thread
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
assert.containsOnce(
document.body,
'.o_Message_subject',
"should display subject of the message"
);
assert.strictEqual(
document.querySelector('.o_Message_subject').textContent,
"Subject: Salutations, voyageur",
"Subject of the message should be 'Salutations, voyageur'"
);
});
QUnit.test('click on (non-channel/non-partner) origin thread link should redirect to form view', async function (assert) {
assert.expect(9);
const pyEnv = await startServer();
const resFakeId1 = pyEnv['res.fake'].create({ name: 'Some record' });
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'res.fake',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resFakeId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, env, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
patchWithCleanup(env.services.action, {
doAction(action) {
// Callback of doing an action (action manager).
// Expected to be called on click on origin thread link,
// which redirects to form view of record related to origin thread
assert.step('do-action');
assert.strictEqual(
action.type,
'ir.actions.act_window',
"action should open a view"
);
assert.deepEqual(
action.views,
[[false, 'form']],
"action should open form view"
);
assert.strictEqual(
action.res_model,
'res.fake',
"action should open view with model 'res.fake' (model of message origin thread)"
);
assert.strictEqual(
action.res_id,
resFakeId1,
"action should open view with id of resFake1 (id of message origin thread)"
);
return Promise.resolve();
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
assert.containsOnce(
document.body,
'.o_Message_originThreadLink',
"should display origin thread link"
);
assert.strictEqual(
document.querySelector('.o_Message_originThreadLink').textContent,
"Some record",
"origin thread link should display record name"
);
document.querySelector('.o_Message_originThreadLink').click();
assert.verifySteps(['do-action'], "should have made an action on click on origin thread (to open form view)");
});
QUnit.test('subject should not be shown when subject is the same as the thread name', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" });
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'mail.channel',
res_id: mailChannelId1,
needaction: true,
subject: "Salutations, voyageur",
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsNone(
document.body,
'.o_Message_subject',
"subject should not be shown when subject is the same as the thread name"
);
});
QUnit.test('subject should not be shown when subject is the same as the thread name and both have the same prefix', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Re: Salutations, voyageur" });
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'mail.channel',
res_id: mailChannelId1,
needaction: true,
subject: "Re: Salutations, voyageur",
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsNone(
document.body,
'.o_Message_subject',
"subject should not be shown when subject is the same as the thread name and both have the same prefix"
);
});
QUnit.test('subject should not be shown when subject differs from thread name only by the "Re:" prefix', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" });
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'mail.channel',
res_id: mailChannelId1,
needaction: true,
subject: "Re: Salutations, voyageur",
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsNone(
document.body,
'.o_Message_subject',
"should not display subject when subject differs from thread name only by the 'Re:' prefix"
);
});
QUnit.test('subject should not be shown when subject differs from thread name only by the "Fw:" and "Re:" prefix', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" });
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'mail.channel',
res_id: mailChannelId1,
needaction: true,
subject: "Fw: Re: Salutations, voyageur",
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsNone(
document.body,
'.o_Message_subject',
"should not display subject when subject differs from thread name only by the 'Fw:' and Re:' prefix"
);
});
QUnit.test('subject should be shown when the thread name has an extra prefix compared to subject', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Re: Salutations, voyageur" });
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'mail.channel',
res_id: mailChannelId1,
needaction: true,
subject: "Salutations, voyageur",
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsOnce(
document.body,
'.o_Message_subject',
"subject should be shown when the thread name has an extra prefix compared to subject"
);
});
QUnit.test('subject should not be shown when subject differs from thread name only by the "fw:" prefix and both contain another common prefix', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Re: Salutations, voyageur" });
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'mail.channel',
res_id: mailChannelId1,
needaction: true,
subject: "fw: re: Salutations, voyageur",
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsNone(
document.body,
'.o_Message_subject',
"subject should not be shown when subject differs from thread name only by the 'fw:' prefix and both contain another common prefix"
);
});
QUnit.test('subject should not be shown when subject differs from thread name only by the "Re: Re:" prefix', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" });
const mailMessageId1 = pyEnv['mail.message'].create({
body: "not empty",
model: 'mail.channel',
res_id: mailChannelId1,
needaction: true,
subject: "Re: Re: Salutations, voyageur",
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging, openDiscuss } = await start();
await afterEvent({
eventName: 'o-thread-view-hint-processed',
func: openDiscuss,
message: "should wait until inbox displayed its messages",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread === messaging.inbox.thread
);
},
});
assert.containsNone(
document.body,
'.o_Message_subject',
"should not display subject when subject differs from thread name only by the 'Re: Re:'' prefix"
);
});
});
});

View file

@ -0,0 +1,34 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_message_edit_tests.js');
QUnit.test('click on message edit button should open edit composer', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
message_type: 'comment',
model: 'mail.channel',
res_id: mailChannelId1,
});
const { click, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
await click('.o_Message');
await click('.o_MessageActionView_actionEdit');
assert.containsOnce(document.body, '.o_Message_composer', 'click on message edit button should open edit composer');
});
});
});

View file

@ -0,0 +1,170 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_pinned_tests.js');
QUnit.test('sidebar: pinned channel 1: init with one pinned channel', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
`.o_Discuss_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`,
"The Inbox is opened in discuss"
);
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`,
"should have the only channel of which user is member in discuss sidebar"
);
});
QUnit.test('sidebar: pinned channel 2: open pinned channel', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const { click, openDiscuss } = await start();
await openDiscuss();
await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`);
assert.containsOnce(
document.body,
`.o_Discuss_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`,
"The channel #General is displayed in discuss"
);
});
QUnit.test('sidebar: pinned channel 3: open channel and leave it', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [[0, 0, {
fold_state: 'open',
is_minimized: true,
partner_id: pyEnv.currentPartnerId,
}]],
});
const { click, openDiscuss } = await start({
async mockRPC(route, args) {
if (args.method === 'action_unfollow') {
assert.step('action_unfollow');
assert.deepEqual(args.args[0], [mailChannelId1],
"The right id is sent to the server to remove"
);
}
},
});
await openDiscuss();
await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`);
assert.verifySteps([], "action_unfollow is not called yet");
await click('.o_DiscussSidebarCategoryItem_commandLeave');
assert.verifySteps(
[
'action_unfollow'
],
"action_unfollow has been called when leaving a channel"
);
assert.containsNone(
document.body,
`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`,
"The channel must have been removed from discuss sidebar"
);
assert.containsOnce(
document.body,
'.o_Discuss_noThread',
"should have no thread opened in discuss"
);
});
QUnit.test('sidebar: unpin channel from bus', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const { click, messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
`.o_Discuss_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`,
"The Inbox is opened in discuss"
);
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`,
"1 channel is present in discuss sidebar and it is 'general'"
);
await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`);
assert.containsOnce(
document.body,
`.o_Discuss_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`,
"The channel #General is opened in discuss"
);
// Simulate receiving a leave channel notification
// (e.g. from user interaction from another device or browser tab)
await afterNextRender(() => {
pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.channel/unpin', {
'id': mailChannelId1,
});
});
assert.containsOnce(
document.body,
'.o_Discuss_noThread',
"should have no thread opened in discuss"
);
assert.containsNone(
document.body,
`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`,
"The channel must have been removed from discuss sidebar"
);
});
QUnit.test('[technical] sidebar: channel group_based_subscription: mandatorily pinned', async function (assert) {
assert.expect(2);
// FIXME: The following is admittedly odd.
// Fixing it should entail a deeper reflexion on the group_based_subscription
// and is_pinned functionalities, especially in python.
// task-2284357
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [[0, 0, {
is_pinned: false,
partner_id: pyEnv.currentPartnerId,
}]],
group_based_subscription: true,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`,
"The channel #General is in discuss sidebar"
);
assert.containsNone(
document.body,
'o_DiscussSidebarCategoryItem_commandLeave',
"The group_based_subscription channel is not unpinnable"
);
});
});
});

View file

@ -0,0 +1,168 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { datetime_to_str } from 'web.time';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_sidebar_category_item_tests.js');
QUnit.test('channel - avatar: should have correct avatar', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ avatarCacheKey: '100111' });
const { openDiscuss } = await start();
await openDiscuss();
const channelItem = document.querySelector(`
.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]
`);
assert.strictEqual(
channelItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length,
1,
"channel should have an avatar"
);
assert.strictEqual(
channelItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src,
`/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=100111`,
'should link to the correct picture source'
);
});
QUnit.test('channel - avatar: should update avatar url from bus', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ avatarCacheKey: '101010' });
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelector(`
.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]
.o_DiscussSidebarCategoryItem_image`).dataset.src,
`/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=101010`,
);
await afterNextRender(() => {
messaging.rpc({
model: 'mail.channel',
method: 'write',
args: [[mailChannelId1], { image_128: 'This field does not matter' }],
});
});
const result = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]]);
const newCacheKey = result[0]['avatarCacheKey'];
assert.strictEqual(
document.querySelector(`
.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]
.o_DiscussSidebarCategoryItem_image`).dataset.src,
`/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=${newCacheKey}`,
);
});
QUnit.test('chat - avatar: should have correct avatar', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo", im_status: 'offline' });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'chat',
});
const { openDiscuss } = await start();
await openDiscuss();
const chatItem = document.querySelector(`
.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]
`);
assert.strictEqual(
chatItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length,
1,
"chat should have an avatar"
);
assert.strictEqual(
chatItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src,
`/web/image/res.partner/${resPartnerId1}/avatar_128`,
'should link to the partner avatar'
);
});
QUnit.test('chat - sorting: should be sorted by last activity time', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([
{
channel_member_ids: [[0, 0, {
last_interest_dt: datetime_to_str(new Date(2021, 0, 1)),
partner_id: pyEnv.currentPartnerId,
}]],
channel_type: 'chat',
},
{
channel_member_ids: [[0, 0, {
last_interest_dt: datetime_to_str(new Date(2021, 0, 2)),
partner_id: pyEnv.currentPartnerId,
}]],
channel_type: 'chat',
},
]);
const { click, openDiscuss } = await start();
await openDiscuss();
const initialChats = document.querySelectorAll('.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_item');
assert.strictEqual(
initialChats.length,
2,
"should have 2 livechat items"
);
assert.strictEqual(
Number(initialChats[0].dataset.channelId),
mailChannelId2,
"first livechat should be the one with the more recent last activity time"
);
assert.strictEqual(
Number(initialChats[1].dataset.channelId),
mailChannelId1,
"second chat should be the one with the less recent last activity time"
);
// post a new message on the last channel
await afterNextRender(() => initialChats[1].click());
await afterNextRender(() => document.execCommand('insertText', false, "Blabla"));
await click('.o_Composer_buttonSend');
const newChats = document.querySelectorAll('.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_item');
assert.strictEqual(
newChats.length,
2,
"should have 2 chat items"
);
assert.strictEqual(
Number(newChats[0].dataset.channelId),
mailChannelId1,
"first chat should be the one with the more recent last activity time"
);
assert.strictEqual(
Number(newChats[1].dataset.channelId),
mailChannelId2,
"second chat should be the one with the less recent last activity time"
);
});
});
});

View file

@ -0,0 +1,732 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_sidebar_category_tests.js');
QUnit.test('channel - counter: should not have a counter if the category is unfolded and without needaction messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({});
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_counter`).length,
0,
"should not have a counter if the category is unfolded and without unread messages"
);
});
QUnit.test('channel - counter: should not have a counter if the category is unfolded and with needaction messagens', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }]);
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
{
body: "message 1",
model: "mail.channel",
res_id: mailChannelId1,
},
{
body: "message_2",
model: "mail.channel",
res_id: mailChannelId2,
},
]);
pyEnv['mail.notification'].create([
{
mail_message_id: mailMessageId1,
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
},
{
mail_message_id: mailMessageId2,
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
},
]);
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_counter`).length,
0,
"should not have a counter if the category is unfolded and with needaction messages",
);
});
QUnit.test('channel - counter: should not have a counter if category is folded and without needaction messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: false,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_counter`).length,
0,
"should not have a counter if the category is folded and without unread messages"
);
});
QUnit.test('channel - counter: should have correct value of needaction threads if category is folded and with needaction messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }]);
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
{
body: "message 1",
model: "mail.channel",
res_id: mailChannelId1,
},
{
body: "message_2",
model: "mail.channel",
res_id: mailChannelId2,
},
]);
pyEnv['mail.notification'].create([
{
mail_message_id: mailMessageId1,
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
},
{
mail_message_id: mailMessageId2,
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
},
]);
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: false,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelector(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_counter`).textContent,
"2",
"should have correct value of needaction threads if category is folded and with needaction messages"
);
});
QUnit.test('channel - command: should have view command when category is unfolded', async function (assert) {
assert.expect(1);
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView`).length,
1,
"should have view command when channel category is open"
);
});
QUnit.test('channel - command: should have view command when category is folded', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: false,
});
const { click, openDiscuss } = await start();
await openDiscuss();
await click(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_title`);
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView`).length,
1,
"should have view command when channel category is closed"
);
});
QUnit.test('channel - command: should have add command when category is unfolded', async function (assert) {
assert.expect(1);
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length,
1,
"should have add command when channel category is open"
);
});
QUnit.test('channel - command: should not have add command when category is folded', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: false,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length,
0,
"should not have add command when channel category is closed"
);
});
QUnit.test('channel - states: close manually by clicking the title', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: true,
});
const { click, openDiscuss } = await start();
await openDiscuss();
await click(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_title`);
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category channel should be closed and the content should be invisible"
);
});
QUnit.test('channel - states: open manually by clicking the title', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: false,
});
const { click, openDiscuss } = await start();
await openDiscuss();
await click(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_title`);
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category channel should be open and the content should be visible"
);
});
QUnit.test('channel - states: close should update the value on the server', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: true,
});
const currentUserId = pyEnv.currentUserId;
const { click, messaging, openDiscuss } = await start();
await openDiscuss();
const initalSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
initalSettings.is_discuss_sidebar_category_channel_open,
true,
"the server side value should be true"
);
await click(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_title`);
const newSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
newSettings.is_discuss_sidebar_category_channel_open,
false,
"the server side value should be false"
);
});
QUnit.test('channel - states: open should update the value on the server', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: false,
});
const currentUserId = pyEnv.currentUserId;
const { click, messaging, openDiscuss } = await start();
await openDiscuss();
const initalSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
initalSettings.is_discuss_sidebar_category_channel_open,
false,
"the server side value should be false"
);
await click(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_title`);
const newSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
newSettings.is_discuss_sidebar_category_channel_open,
true,
"the server side value should be false"
);
});
QUnit.test('channel - states: close from the bus', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const resUsersSettingsId1 = pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: true,
});
const { openDiscuss } = await start();
await openDiscuss();
await afterNextRender(() => {
pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'res.users.settings/insert', {
id: resUsersSettingsId1,
'is_discuss_sidebar_category_channel_open': false,
});
});
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category channel should be closed and the content should be invisible"
);
});
QUnit.test('channel - states: open from the bus', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const resUsersSettingsId1 = pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_channel_open: false,
});
const { openDiscuss } = await start();
await openDiscuss();
await afterNextRender(() => {
pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'res.users.settings/insert', {
id: resUsersSettingsId1,
'is_discuss_sidebar_category_channel_open': true,
});
});
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category channel should be open and the content should be visible"
);
});
QUnit.test('channel - states: the active category item should be visible even if the category is closed', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const { click, messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`
);
const channel = document.querySelector(`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`);
await afterNextRender(() => {
channel.click();
});
assert.ok(channel.classList.contains('o-active'));
await click(`.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_title`);
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
'the active channel item should remain even if the category is folded'
);
await click(`.o_DiscussSidebarMailbox[data-mailbox-local-id="${
messaging.inbox.localId
}"]`);
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"inactive item should be invisible if the category is folded"
);
});
QUnit.test('chat - counter: should not have a counter if the category is unfolded and without unread messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, {
message_unread_counter: 0,
partner_id: pyEnv.currentPartnerId,
}],
],
channel_type: 'chat',
});
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_counter`).length,
0,
"should not have a counter if the category is unfolded and without unread messages",
);
});
QUnit.test('chat - counter: should not have a counter if the category is unfolded and with unread messagens', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, {
message_unread_counter: 10,
partner_id: pyEnv.currentPartnerId,
}],
],
channel_type: 'chat',
});
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_counter`).length,
0,
"should not have a counter if the category is unfolded and with unread messages",
);
});
QUnit.test('chat - counter: should not have a counter if category is folded and without unread messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, {
message_unread_counter: 0,
partner_id: pyEnv.currentPartnerId,
}],
],
channel_type: 'chat',
});
const { click, openDiscuss } = await start();
await openDiscuss();
await click(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_title`);
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_counter`).length,
0,
"should not have a counter if the category is folded and without unread messages"
);
});
QUnit.test('chat - counter: should have correct value of unread threads if category is folded and with unread messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create([
{
channel_member_ids: [
[0, 0, {
message_unread_counter: 10,
partner_id: pyEnv.currentPartnerId,
}],
],
channel_type: 'chat',
},
{
channel_member_ids: [
[0, 0, {
message_unread_counter: 20,
partner_id: pyEnv.currentPartnerId,
}],
],
channel_type: 'chat',
},
]);
const { click, openDiscuss } = await start();
await openDiscuss();
await click(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_title`);
assert.strictEqual(
document.querySelector(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_counter`).textContent,
"2",
"should have correct value of unread threads if category is folded and with unread messages"
);
});
QUnit.test('chat - command: should have add command when category is unfolded', async function (assert) {
assert.expect(1);
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length,
1,
"should have add command when chat category is open"
);
});
QUnit.test('chat - command: should not have add command when category is folded', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_chat_open: false,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length,
0,
"should not have add command when chat category is closed"
);
});
QUnit.test('chat - states: close manually by clicking the title', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_type: 'chat',
});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_chat_open: true,
});
const { click, openDiscuss } = await start();
await openDiscuss();
await click(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_title`);
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category chat should be closed and the content should be invisible"
);
});
QUnit.test('chat - states: open manually by clicking the title', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_type: 'chat',
});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_chat_open: false,
});
const { click, openDiscuss } = await start();
await openDiscuss();
await click(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_title`);
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category chat should be open and the content should be visible"
);
});
QUnit.test('chat - states: close should call update server data', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_chat_open: true,
});
const currentUserId = pyEnv.currentUserId;
const { click, messaging, openDiscuss } = await start();
await openDiscuss();
const initalSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
initalSettings.is_discuss_sidebar_category_chat_open,
true,
"the value in server side should be true"
);
await click(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_title`);
const newSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
newSettings.is_discuss_sidebar_category_chat_open,
false,
"the value in server side should be false"
);
});
QUnit.test('chat - states: open should call update server data', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_chat_open: false,
});
const { click, messaging, openDiscuss } = await start();
await openDiscuss();
const initalSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[pyEnv.currentUserId]],
});
assert.strictEqual(
initalSettings.is_discuss_sidebar_category_chat_open,
false,
"the value in server side should be false"
);
await click(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_title`);
const newSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[pyEnv.currentUserId]],
});
assert.strictEqual(
newSettings.is_discuss_sidebar_category_chat_open,
true,
"the value in server side should be true"
);
});
QUnit.test('chat - states: close from the bus', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_type: 'chat',
});
const resUsersSettingsId1 = pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_chat_open: true,
});
const { openDiscuss } = await start();
await openDiscuss();
await afterNextRender(() => {
pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'res.users.settings/insert', {
id: resUsersSettingsId1,
'is_discuss_sidebar_category_chat_open': false,
});
});
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category chat should be close and the content should be invisible"
);
});
QUnit.test('chat - states: open from the bus', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_type: 'chat',
});
const resUsersSettingsId1 = pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_chat_open: false,
});
const { openDiscuss } = await start();
await openDiscuss();
await afterNextRender(() => {
pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'res.users.settings/insert', {
id: resUsersSettingsId1,
'is_discuss_sidebar_category_chat_open': true,
});
});
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category chat should be open and the content should be visible"
);
});
QUnit.test('chat - states: the active category item should be visible even if the category is closed', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_type: 'chat',
});
const { click, messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`
);
const chat = document.querySelector(`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`);
await afterNextRender(() => {
chat.click();
});
assert.ok(chat.classList.contains('o-active'));
await click(`.o_DiscussSidebar_categoryChat .o_DiscussSidebarCategory_title`);
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
'the active chat item should remain even if the category is folded'
);
await click(`.o_DiscussSidebarMailbox[data-mailbox-local-id="${
messaging.inbox.localId
}"]`);
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"inactive item should be invisible if the category is folded"
);
});
});
});

View file

@ -0,0 +1,133 @@
/** @odoo-module **/
import { makeDeferred } from '@mail/utils/deferred';
import {
nextAnimationFrame,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_sidebar_tests.js');
QUnit.test('sidebar find shows channels matching search term', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
channel_member_ids: [],
channel_type: 'channel',
group_public_id: false,
name: 'test',
});
const searchReadDef = makeDeferred();
const { click, openDiscuss } = await start({
async mockRPC(route, args) {
if (args.method === 'search_read') {
searchReadDef.resolve();
}
},
});
await openDiscuss();
await click(`.o_DiscussSidebarCategory_commandAdd`);
document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus();
document.execCommand('insertText', false, "test");
document.querySelector(`.o_DiscussSidebarCategory_addingItem`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_DiscussSidebarCategory_addingItem`)
.dispatchEvent(new window.KeyboardEvent('keyup'));
await searchReadDef;
await nextAnimationFrame(); // ensures search_read rpc is rendered.
const results = document.querySelectorAll('.ui-autocomplete .ui-menu-item a');
assert.ok(
results,
"should have autocomplete suggestion after typing on 'find or create channel' input"
);
assert.strictEqual(
results.length,
// When searching for a single existing channel, the results list will have at least 2 lines:
// One for the existing channel itself
// One for creating a channel with the search term
2
);
assert.strictEqual(
results[0].textContent,
"test",
"autocomplete suggestion should target the channel matching search term"
);
});
QUnit.test('sidebar find shows channels matching search term even when user is member', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
],
channel_type: 'channel',
group_public_id: false,
name: 'test',
});
const searchReadDef = makeDeferred();
const { click, openDiscuss } = await start({
async mockRPC(route, args) {
if (args.method === 'search_read') {
searchReadDef.resolve();
}
},
});
await openDiscuss();
await click(`.o_DiscussSidebarCategory_commandAdd`);
document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus();
document.execCommand('insertText', false, "test");
document.querySelector(`.o_DiscussSidebarCategory_addingItem`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_DiscussSidebarCategory_addingItem`)
.dispatchEvent(new window.KeyboardEvent('keyup'));
await searchReadDef;
await nextAnimationFrame();
const results = document.querySelectorAll('.ui-autocomplete .ui-menu-item a');
assert.ok(
results,
"should have autocomplete suggestion after typing on 'find or create channel' input"
);
assert.strictEqual(
results.length,
// When searching for a single existing channel, the results list will have at least 2 lines:
// One for the existing channel itself
// One for creating a channel with the search term
2
);
assert.strictEqual(
results[0].textContent,
"test",
"autocomplete suggestion should target the channel matching search term even if user is member"
);
});
QUnit.test('sidebar channels should be ordered case insensitive alphabetically', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create([
{ name: "Xyz" },
{ name: "abc" },
{ name: "Abc" },
{ name: "Xyz" },
]);
const { openDiscuss } = await start();
await openDiscuss();
const results = document.querySelectorAll('.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategoryItem_name');
assert.deepEqual(
[results[0].textContent, results[1].textContent, results[2].textContent, results[3].textContent],
["abc", "Abc", "Xyz", "Xyz"],
"Channel name should be in case insensitive alphabetical order"
);
});
});
});

View file

@ -0,0 +1,40 @@
/**@odoo-module **/
import { getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
QUnit.module("Field text emojis", (hooks) => {
let target = undefined;
let serverData = undefined;
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
foo: { type: "char" }
},
records: [{ id: 1 }]
}
}
};
setupViewRegistries();
});
QUnit.test("emojis button is not shown in readonly", async (assert) => {
await makeView({
type: "form",
resId: 1,
resModel: "partner",
arch: `<form><field name="foo" widget="text_emojis" /></form>`,
serverData
});
assert.containsOnce(target, ".o_field_text_emojis");
assert.containsOnce(target, ".o_field_text_emojis button");
assert.isVisible(target.querySelector(".o_field_text_emojis button"));
assert.isVisible(target, ".o_field_text_emojis button .fa-smile");
});
});

View file

@ -0,0 +1,183 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('follow_button_tests.js');
QUnit.test('base rendering not editable', async function (assert) {
assert.expect(2);
const { openView, pyEnv } = await start();
await openView({
res_id: pyEnv.currentPartnerId,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_FollowButton',
"should have follow button component"
);
assert.containsOnce(
document.body,
'.o_FollowButton_follow',
"should have 'Follow' button"
);
});
QUnit.test('hover following button', async function (assert) {
assert.expect(8);
const pyEnv = await startServer();
const threadId = pyEnv['res.partner'].create({});
const followerId = pyEnv['mail.followers'].create({
is_active: true,
partner_id: pyEnv.currentPartnerId,
res_id: threadId,
res_model: 'res.partner',
});
pyEnv['res.partner'].write([pyEnv.currentPartnerId], {
message_follower_ids: [followerId],
});
const { openView } = await start();
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_FollowButton',
"should have follow button component"
);
assert.containsOnce(
document.body,
'.o_FollowButton_unfollow',
"should have 'Unfollow' button"
);
assert.strictEqual(
document.querySelector('.o_FollowButton_text').textContent.trim(),
'Following',
"'unfollow' button should display 'Following' as text when not hovered"
);
assert.containsNone(
document.querySelector('.o_FollowButton_unfollow'),
'.fa-times',
"'unfollow' button should not contain a cross icon when not hovered"
);
assert.containsOnce(
document.querySelector('.o_FollowButton_unfollow'),
'.fa-check',
"'unfollow' button should contain a check icon when not hovered"
);
await afterNextRender(() => {
document
.querySelector('.o_FollowButton_unfollow')
.dispatchEvent(new window.MouseEvent('mouseenter'));
}
);
assert.strictEqual(
document.querySelector('.o_FollowButton_text').textContent.trim(),
'Unfollow',
"'unfollow' button should display 'Unfollow' as text when hovered"
);
assert.containsOnce(
document.querySelector('.o_FollowButton_unfollow'),
'.fa-times',
"'unfollow' button should contain a cross icon when hovered"
);
assert.containsNone(
document.querySelector('.o_FollowButton_unfollow'),
'.fa-check',
"'unfollow' button should not contain a check icon when hovered"
);
});
QUnit.test('click on "follow" button', async function (assert) {
assert.expect(4);
const { click, openView, pyEnv } = await start();
await openView({
res_id: pyEnv.currentPartnerId,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_FollowButton',
"should have follow button component"
);
assert.containsOnce(
document.body,
'.o_FollowButton_follow',
"should have button follow"
);
await click('.o_FollowButton_follow');
assert.containsNone(
document.body,
'.o_FollowButton_follow',
"should not have follow button after clicked on follow"
);
assert.containsOnce(
document.body,
'.o_FollowButton_unfollow',
"should have unfollow button after clicked on follow"
);
});
QUnit.test('click on "unfollow" button', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const threadId = pyEnv['res.partner'].create({});
pyEnv['mail.followers'].create({
is_active: true,
partner_id: pyEnv.currentPartnerId,
res_id: threadId,
res_model: 'res.partner',
});
const { click, openView } = await start();
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_FollowButton',
"should have follow button component"
);
assert.containsNone(
document.body,
'.o_FollowButton_follow',
"should not have button follow"
);
assert.containsOnce(
document.body,
'.o_FollowButton_unfollow',
"should have button unfollow"
);
await click('.o_FollowButton_unfollow');
assert.containsOnce(
document.body,
'.o_FollowButton_follow',
"should have follow button after clicked on unfollow"
);
assert.containsNone(
document.body,
'.o_FollowButton_unfollow',
"should not have unfollow button after clicked on unfollow"
);
});
});
});

View file

@ -0,0 +1,416 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('follower_list_menu_tests.js');
QUnit.test('base rendering editable', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, openView } = await start({
async mockRPC(route, args, performRPC) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await performRPC(route, args);
res['hasWriteAccess'] = true;
return res;
}
},
});
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_FollowerListMenu',
"should have followers menu component"
);
assert.containsOnce(
document.body,
'.o_FollowerListMenu_buttonFollowers',
"should have followers button"
);
assert.notOk(
document.querySelector('.o_FollowerListMenu_buttonFollowers').disabled,
"followers button should not be disabled"
);
assert.containsNone(
document.body,
'.o_FollowerListMenu_dropdown',
"followers dropdown should not be opened"
);
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_FollowerListMenu_dropdown',
"followers dropdown should be opened"
);
});
QUnit.test('click on "add followers" button', async function (assert) {
assert.expect(15);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([
{ name: 'resPartner1' },
{ name: 'resPartner2' },
{ name: 'resPartner3' },
]);
pyEnv['mail.followers'].create({
partner_id: resPartnerId2,
email: "bla@bla.bla",
is_active: true,
name: "François Perusse",
res_id: resPartnerId1,
res_model: 'res.partner',
});
const { click, env, openView } = await start({
async mockRPC(route, args, performRPC) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await performRPC(route, args);
res['hasWriteAccess'] = true;
return res;
}
},
});
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
patchWithCleanup(env.services.action, {
doAction(action, options) {
assert.step('action:open_view');
assert.strictEqual(
action.context.default_res_model,
'res.partner',
"'The 'add followers' action should contain thread model in context'"
);
assert.strictEqual(
action.context.default_res_id,
resPartnerId1,
"The 'add followers' action should contain thread id in context"
);
assert.strictEqual(
action.res_model,
'mail.wizard.invite',
"The 'add followers' action should be a wizard invite of mail module"
);
assert.strictEqual(
action.type,
"ir.actions.act_window",
"The 'add followers' action should be of type 'ir.actions.act_window'"
);
pyEnv['mail.followers'].create({
partner_id: resPartnerId3,
email: "bla@bla.bla",
is_active: true,
name: "Wololo",
res_id: resPartnerId1,
res_model: 'res.partner',
});
options.onClose();
},
});
assert.containsOnce(
document.body,
'.o_FollowerListMenu',
"should have followers menu component"
);
assert.containsOnce(
document.body,
'.o_FollowerListMenu_buttonFollowers',
"should have followers button"
);
assert.strictEqual(
document.querySelector('.o_FollowerListMenu_buttonFollowersCount').textContent,
"1",
"Followers counter should be equal to 1"
);
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_FollowerListMenu_dropdown',
"followers dropdown should be opened"
);
assert.containsOnce(
document.body,
'.o_FollowerListMenu_addFollowersButton',
"followers dropdown should contain a 'Add followers' button"
);
await click('.o_FollowerListMenu_addFollowersButton');
assert.containsNone(
document.body,
'.o_FollowerListMenu_dropdown',
"followers dropdown should be closed after click on 'Add followers'"
);
assert.verifySteps([
'action:open_view',
]);
assert.strictEqual(
document.querySelector('.o_FollowerListMenu_buttonFollowersCount').textContent,
"2",
"Followers counter should now be equal to 2"
);
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsN(
document.body,
'.o_FollowerMenu_follower',
2,
"Follower list should be refreshed and contain 2 followers"
);
assert.strictEqual(
document.querySelector('.o_Follower_name').textContent,
"François Perusse",
"Follower added in follower list should be the one added"
);
});
QUnit.test('click on remove follower', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([
{ name: 'resPartner1' },
{ name: 'resPartner2' },
]);
pyEnv['mail.followers'].create({
partner_id: resPartnerId2,
email: "bla@bla.bla",
is_active: true,
name: "Wololo",
res_id: resPartnerId1,
res_model: 'res.partner',
});
const { click, openView } = await start({
async mockRPC(route, args, performRPC) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await performRPC(route, args);
res['hasWriteAccess'] = true;
return res;
}
if (route.includes('message_unsubscribe')) {
assert.step('message_unsubscribe');
assert.deepEqual(
args.args,
[[resPartnerId1], [resPartnerId2]],
"message_unsubscribe should be called with right argument"
);
}
},
});
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
"should have follower component"
);
assert.containsOnce(
document.body,
'.o_Follower_removeButton',
"should display a remove button"
);
await click('.o_Follower_removeButton');
assert.verifySteps(
['message_unsubscribe'],
"clicking on remove button should call 'message_unsubscribe' route"
);
assert.containsNone(
document.body,
'.o_Follower',
"should no longer have follower component"
);
});
QUnit.test('Hide "Add follower" and subtypes edition/removal buttons except own user on read only record', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{ name: "resPartner1" }, { name: "resPartner2" }]);
pyEnv['mail.followers'].create([
{
name: "Jean Michang",
is_active: true,
partner_id: pyEnv.currentPartnerId,
res_id: resPartnerId1,
res_model: 'res.partner',
},
{
name: "Eden Hazard",
is_active: true,
partner_id: resPartnerId2,
res_id: resPartnerId1,
res_model: 'res.partner',
},
]);
const { click, openView } = await start({
async mockRPC(route, args, performRPC) {
if (route === '/mail/thread/data') {
// mimic user with no write access
const res = await performRPC(route, args);
res['hasWriteAccess'] = false;
return res;
}
},
});
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsNone(
document.body,
'.o_FollowerListMenu_addFollowersButton',
"'Add followers' button should not be displayed for a readonly record",
);
const followersList = document.querySelectorAll('.o_Follower');
assert.containsOnce(
followersList[0],
'.o_Follower_editButton',
"should display edit button for a follower related to current user",
);
assert.containsOnce(
followersList[0],
'.o_Follower_removeButton',
"should display remove button for a follower related to current user",
);
assert.containsNone(
followersList[1],
'.o_Follower_editButton',
"should not display edit button for other followers on a readonly record",
);
assert.containsNone(
followersList[1],
'.o_Follower_removeButton',
"should not display remove button for others on a readonly record",
);
});
QUnit.test('Show "Add follower" and subtypes edition/removal buttons on all followers if user has write access', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{ name: "resPartner1" }, { name: "resPartner2" }]);
pyEnv['mail.followers'].create([
{
name: "Jean Michang",
is_active: true,
partner_id: pyEnv.currentPartnerId,
res_id: resPartnerId1,
res_model: 'res.partner',
},
{
name: "Eden Hazard",
is_active: true,
partner_id: resPartnerId2,
res_id: resPartnerId1,
res_model: 'res.partner',
},
]);
const { click, openView } = await start({
async mockRPC(route, args, performRPC) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await performRPC(...arguments);
res['hasWriteAccess'] = true;
return res;
}
},
});
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_FollowerListMenu_addFollowersButton',
"'Add followers' button should be displayed for the writable record",
);
const followersList = document.querySelectorAll('.o_Follower');
assert.containsOnce(
followersList[0],
'.o_Follower_editButton',
"should display edit button for a follower related to current user",
);
assert.containsOnce(
followersList[0],
'.o_Follower_removeButton',
"should display remove button for a follower related to current user",
);
assert.containsOnce(
followersList[1],
'.o_Follower_editButton',
"should display edit button for other followers also on the writable record",
);
assert.containsOnce(
followersList[1],
'.o_Follower_removeButton',
"should display remove button for other followers also on the writable record",
);
});
QUnit.test('Show "No Followers" dropdown-item if there are no followers and user dose not have write access', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { click, openView } = await start({
async mockRPC(route, args, performRPC) {
if (route === '/mail/thread/data') {
// mimic user without write access
const res = await performRPC(route, args);
res['hasWriteAccess'] = false;
return res;
}
},
});
await openView({
res_id: resPartnerId1,
res_model: 'res.partner',
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_FollowerListMenu_noFollowers.disabled',
"should display 'No Followers' dropdown-item",
);
});
});
});

View file

@ -0,0 +1,169 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('follower_subtype_tests.js');
QUnit.test('simplest layout of a followed subtype', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const subtypeId = pyEnv['mail.message.subtype'].create({
default: true,
name: 'TestSubtype',
});
const followerId = pyEnv['mail.followers'].create({
display_name: "François Perusse",
partner_id: pyEnv.currentPartnerId,
res_model: 'res.partner',
res_id: pyEnv.currentPartnerId,
subtype_ids: [subtypeId],
});
pyEnv['res.partner'].write([pyEnv.currentPartnerId], {
message_follower_ids: [followerId],
});
const { click, openView } = await start({
// FIXME: should adapt mock server code to provide `hasWriteAccess`
async mockRPC(route, args, performRPC) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await performRPC(...arguments);
res['hasWriteAccess'] = true;
return res;
}
},
});
await openView({
res_model: 'res.partner',
res_id: pyEnv.currentPartnerId,
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
await click('.o_Follower_editButton');
assert.containsOnce(
document.body,
'.o_FollowerSubtype:contains(TestSubtype)',
"should have a follower subtype for 'TestSubtype'"
);
assert.containsOnce(
document.querySelector('.o_FollowerSubtype'),
'.o_FollowerSubtype_label',
"should have a label"
);
assert.containsOnce(
$('.o_FollowerSubtype:contains(TestSubtype)'),
'.o_FollowerSubtype_checkbox',
"should have a checkbox"
);
assert.strictEqual(
$('.o_FollowerSubtype:contains(TestSubtype) .o_FollowerSubtype_label')[0].textContent,
"TestSubtype",
"should have the name of the subtype as label"
);
assert.ok(
$('.o_FollowerSubtype:contains(TestSubtype) .o_FollowerSubtype_checkbox')[0].checked,
"checkbox should be checked as follower subtype is followed"
);
});
QUnit.test('simplest layout of a not followed subtype', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.message.subtype'].create({
default: true,
name: 'TestSubtype',
});
const followerId = pyEnv['mail.followers'].create({
display_name: "François Perusse",
partner_id: pyEnv.currentPartnerId,
res_model: 'res.partner',
res_id: pyEnv.currentPartnerId,
});
pyEnv['res.partner'].write([pyEnv.currentPartnerId], {
message_follower_ids: [followerId],
});
const { click, openView } = await start({
// FIXME: should adapt mock server code to provide `hasWriteAccess`
async mockRPC(route, args, performRPC) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await performRPC(...arguments);
res['hasWriteAccess'] = true;
return res;
}
},
});
await openView({
res_model: 'res.partner',
res_id: pyEnv.currentPartnerId,
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
await click('.o_Follower_editButton');
assert.notOk(
$('.o_FollowerSubtype:contains(TestSubtype) .o_FollowerSubtype_checkbox')[0].checked,
"checkbox should not be checked as follower subtype is not followed"
);
});
QUnit.test('toggle follower subtype checkbox', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const followerSubtypeId = pyEnv['mail.message.subtype'].create({
default: true,
name: 'TestSubtype',
});
const followerId = pyEnv['mail.followers'].create({
display_name: "François Perusse",
partner_id: pyEnv.currentPartnerId,
res_model: 'res.partner',
res_id: pyEnv.currentPartnerId,
});
pyEnv['res.partner'].write([pyEnv.currentPartnerId], {
message_follower_ids: [followerId],
});
const { click, openView } = await start({
// FIXME: should adapt mock server code to provide `hasWriteAccess`
async mockRPC(route, args, performRPC) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await performRPC(...arguments);
res['hasWriteAccess'] = true;
return res;
}
},
});
await openView({
res_model: 'res.partner',
res_id: pyEnv.currentPartnerId,
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
await click('.o_Follower_editButton');
assert.notOk(
document.querySelector(`.o_FollowerSubtype[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtype_checkbox`).checked,
"checkbox should not be checked as follower subtype is not followed"
);
await click(`.o_FollowerSubtype[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtype_checkbox`);
assert.ok(
document.querySelector(`.o_FollowerSubtype[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtype_checkbox`).checked,
"checkbox should now be checked"
);
await click(`.o_FollowerSubtype[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtype_checkbox`);
assert.notOk(
document.querySelector(`.o_FollowerSubtype[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtype_checkbox`).checked,
"checkbox should be no more checked"
);
});
});
});

View file

@ -0,0 +1,366 @@
/** @odoo-module **/
import { makeDeferred } from '@mail/utils/deferred';
import { nextAnimationFrame, start, startServer } from '@mail/../tests/helpers/test_utils';
import { editInput, patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('follower_tests.js');
QUnit.test('base rendering not editable', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const { click, openView } = await start({
async mockRPC(route, args, performRpc) {
if (route === '/mail/thread/data') {
// mimic user without write access
const res = await performRpc(...arguments);
res['hasWriteAccess'] = false;
return res;
}
},
});
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
"should have follower component"
);
assert.containsOnce(
document.body,
'.o_Follower_details',
"should display a details part"
);
assert.containsOnce(
document.body,
'.o_Follower_avatar',
"should display the avatar of the follower"
);
assert.containsOnce(
document.body,
'.o_Follower_name',
"should display the name of the follower"
);
assert.containsNone(
document.body,
'.o_Follower_button',
"should have no button as follower is not editable"
);
});
QUnit.test('base rendering editable', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const { click, openView } = await start();
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
"should have follower component"
);
assert.containsOnce(
document.body,
'.o_Follower_details',
"should display a details part"
);
assert.containsOnce(
document.body,
'.o_Follower_avatar',
"should display the avatar of the follower"
);
assert.containsOnce(
document.body,
'.o_Follower_name',
"should display the name of the follower"
);
assert.containsOnce(
document.body,
'.o_Follower_editButton',
"should have an edit button"
);
assert.containsOnce(
document.body,
'.o_Follower_removeButton',
"should have a remove button"
);
});
QUnit.test('click on partner follower details', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const openFormDef = makeDeferred();
const { click, env, openView } = await start();
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
patchWithCleanup(env.services.action, {
doAction(action) {
assert.step('do_action');
assert.strictEqual(
action.res_id,
partnerId,
"The redirect action should redirect to the right res id (partnerId)"
);
assert.strictEqual(
action.res_model,
'res.partner',
"The redirect action should redirect to the right res model (res.partner)"
);
assert.strictEqual(
action.type,
"ir.actions.act_window",
"The redirect action should be of type 'ir.actions.act_window'"
);
openFormDef.resolve();
},
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
"should have follower component"
);
assert.containsOnce(
document.body,
'.o_Follower_details',
"should display a details part"
);
document.querySelector('.o_Follower_details').click();
await openFormDef;
assert.verifySteps(
['do_action'],
"clicking on follower should redirect to partner form view"
);
});
QUnit.test('click on edit follower', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const { click, messaging, openView } = await start({
async mockRPC(route, args) {
if (route.includes('/mail/read_subscription_data')) {
assert.step('fetch_subtypes');
}
},
});
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
const thread = messaging.models['Thread'].insert({
id: threadId,
model: 'res.partner',
});
await thread.fetchData(['followers']);
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
"should have follower component"
);
assert.containsOnce(
document.body,
'.o_Follower_editButton',
"should display an edit button"
);
await click('.o_Follower_editButton');
assert.verifySteps(
['fetch_subtypes'],
"clicking on edit follower should fetch subtypes"
);
assert.containsOnce(
document.body,
'.o_FollowerSubtypeList',
"A dialog allowing to edit follower subtypes should have been created"
);
});
QUnit.test('edit follower and close subtype dialog', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const { click, openView } = await start({
async mockRPC(route, args) {
if (route.includes('/mail/read_subscription_data')) {
assert.step('fetch_subtypes');
return [{
default: true,
followed: true,
internal: false,
id: 1,
name: "Dummy test",
res_model: 'res.partner'
}];
}
},
});
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
"should have follower component"
);
assert.containsOnce(
document.body,
'.o_Follower_editButton',
"should display an edit button"
);
await click('.o_Follower_editButton');
assert.verifySteps(
['fetch_subtypes'],
"clicking on edit follower should fetch subtypes"
);
assert.containsOnce(
document.body,
'.o_FollowerSubtypeList',
"dialog allowing to edit follower subtypes should have been created"
);
await click('.o_FollowerSubtypeList_closeButton');
assert.containsNone(
document.body,
'.o_DialogManager_dialog',
"follower subtype dialog should be closed after clicking on close button"
);
});
QUnit.test('remove a follower in a dirty form view', async function (assert) {
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.channel'].create({ name: "General", display_name: "General" });
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const views = {
'res.partner,false,form':
`<form>
<field name="name"/>
<field name="channel_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div class="oe_chatter">
<field name="message_ids"/>
<field name="message_follower_ids"/>
</div>
</form>`,
};
const { click, openView } = await start({ serverData: { views } });
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
click("input#channel_ids").catch(() => {});
await nextAnimationFrame();
click(".dropdown-item:contains(General)").catch(() => {});
await nextAnimationFrame();
assert.containsOnce($, ".o_tag:contains(General)");
assert.strictEqual(document.body.querySelector(".o_FollowerListMenu_buttonFollowersCount").innerText, "1");
await editInput(document.body, ".o_field_char[name=name] input", "some value");
await click('.o_FollowerListMenu_buttonFollowers');
await click('.o_FollowerListMenu_dropdown .o_Follower .o_Follower_removeButton');
assert.strictEqual(document.body.querySelector(".o_FollowerListMenu_buttonFollowersCount").innerText, "0");
assert.strictEqual(
document.body.querySelector(".o_field_char[name=name] input").value,
"some value"
);
assert.containsOnce($, ".o_tag:contains(General)");
});
QUnit.test('removing a follower should reload form view', async function (assert) {
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const { click, openView } = await start({
async mockRPC(route, args) {
if (args.method === 'read') {
assert.step(`read ${args.args[0][0]}`);
}
},
});
await openView({
res_id: threadId,
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.verifySteps([`read ${threadId}`]);
await click('.o_FollowerListMenu_buttonFollowers');
await click('.o_FollowerListMenu_dropdown .o_Follower .o_Follower_removeButton');
assert.verifySteps([`read ${threadId}`]);
});
});
});

View file

@ -0,0 +1,384 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('link_preview_tests.js');
const linkPreviewGifPayload = {
og_description: 'test description',
og_image: 'https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif',
og_mimetype: 'image/gif',
og_title: 'Yay Minions GIF - Yay Minions Happiness - Discover & Share GIFs',
og_type: 'video.other',
source_url: 'https://tenor.com/view/yay-minions-happiness-happy-excited-gif-15324023',
};
const linkPreviewCardPayload = {
og_description: 'Description',
og_title: 'Article title',
og_type: 'article',
source_url: 'https://www.odoo.com',
};
const linkPreviewCardImagePayload = {
og_description: 'Description',
og_image: 'https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif',
og_title: 'Article title',
og_type: 'article',
source_url: 'https://www.odoo.com',
};
const linkPreviewVideoPayload = {
og_description: 'Description',
og_image: 'https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif',
og_title: 'video title',
og_type: 'video.other',
source_url: 'https://www.odoo.com',
};
const linkPreviewImagePayload = {
image_mimetype: 'image/jpg',
source_url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg',
};
QUnit.test('auto layout with link preview list', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_Message .o_LinkPreviewListView',
"Should have a link preview list in the DOM"
);
});
QUnit.test('auto layout with link preview as gif', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_LinkPreviewImageView',
"Should have a link preview gif in the DOM"
);
});
QUnit.test('simplest card layout', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardPayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_LinkPreviewCardView',
"should have link preview in DOM"
);
assert.containsOnce(
document.body,
'.o_LinkPreviewCardView_title',
"Should display the link preview title"
);
assert.containsOnce(
document.body,
'.o_LinkPreviewCardView_description',
"Link preview should show the link description"
);
});
QUnit.test('simplest card layout with image', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardImagePayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_LinkPreviewCardView',
"should have link preview in DOM"
);
assert.containsOnce(
document.body,
'.o_LinkPreviewCardView_title',
"Should display the link preview title"
);
assert.containsOnce(
document.body,
'.o_LinkPreviewCardView_description',
"Link preview should show the link description"
);
assert.containsOnce(
document.body,
'.o_LinkPreviewCardView_image',
"Should display an image inside the link preview card"
);
});
QUnit.test('Link preview video layout', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewVideoPayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_LinkPreviewVideoView',
"should have link preview video in DOM"
);
assert.containsOnce(
document.body,
'.o_LinkPreviewVideoView_title',
"Should display the link preview title"
);
assert.containsOnce(
document.body,
'.o_LinkPreviewVideoView_description',
"Link preview should show the link description"
);
assert.containsOnce(
document.body,
'.o_linkPreviewVideo_overlay',
"Should display overlay inside the link preview video image"
);
});
QUnit.test('Link preview image layout', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewImagePayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_LinkPreviewImageView',
"should have link preview image"
);
});
QUnit.test('Remove link preview Gif', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss, click, mouseenter } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await mouseenter('.o_LinkPreviewImageView');
await click('.o_LinkPreviewAside');
assert.containsOnce(
document.body,
'.o_LinkPreviewDeleteConfirmView',
'Should have a link preview confirmation dialog'
);
});
QUnit.test('Remove link preview card', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardPayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss, click, mouseenter } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await mouseenter('.o_LinkPreviewCardView');
await click('.o_LinkPreviewAside');
assert.containsOnce(
document.body,
'.o_LinkPreviewDeleteConfirmView',
'Should have a link preview confirmation dialog'
);
});
QUnit.test('Remove link preview video', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewVideoPayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss, click, mouseenter } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await mouseenter('.o_LinkPreviewVideoView');
await click('.o_LinkPreviewAside');
assert.containsOnce(
document.body,
'.o_LinkPreviewDeleteConfirmView',
'Should have a link preview confirmation dialog'
);
});
QUnit.test('Remove link preview image', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewImagePayload);
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: 'not empty',
link_preview_ids: [linkPreviewId],
message_type: 'comment',
model: "mail.channel",
res_id: mailChannelId,
});
const { openDiscuss, click, mouseenter } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
await mouseenter('.o_LinkPreviewImageView');
await click('.o_LinkPreviewAside');
assert.containsOnce(
document.body,
'.o_LinkPreviewDeleteConfirmView',
'Should have a link preview confirmation dialog'
);
});
});
});

View file

@ -0,0 +1,43 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function() {
QUnit.module('message_in_reply_to_view_tests');
QUnit.test('click on message in reply to highlights the parent message', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
body: "Hey lol",
message_type: 'comment',
model: 'mail.channel',
res_id: mailChannelId1,
});
const mailMessageId2 = pyEnv['mail.message'].create({
body: "Response to Hey lol",
message_type: 'comment',
model: 'mail.channel',
parent_id: mailMessageId1,
res_id: mailChannelId1,
});
const { click, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
await click(`.o_Message[data-id="${mailMessageId2}"] .o_MessageInReplyToView_body`);
assert.containsOnce(
document.body,
`.o-highlighted[data-id="${mailMessageId1}"]`,
"click on message in reply to should highlight the parent message"
);
});
});
});

View file

@ -0,0 +1,271 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('message_seen_indicator_tests.js');
QUnit.test('rendering when just one has received the message', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" });
const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" });
const mailChannelId = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
[0, 0, { partner_id: resPartnerId2 }],
],
channel_type: 'chat', // only chat channel have seen notification
});
const mailMessageId = pyEnv['mail.message'].create({
author_id: pyEnv.currentPartnerId,
body: "<p>Test</p>",
model: 'mail.channel',
res_id: mailChannelId,
});
const [mailChannelMemberId1] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId], ['partner_id', '=', resPartnerId1]]);
pyEnv['mail.channel.member'].write([mailChannelMemberId1], {
fetched_message_id: mailMessageId,
seen_message_id: false,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_MessageSeenIndicator',
"should display a message seen indicator component"
);
assert.doesNotHaveClass(
document.querySelector('.o_MessageSeenIndicator'),
'o-all-seen',
"indicator component should not be considered as all seen"
);
assert.containsOnce(
document.body,
'.o_MessageSeenIndicator_icon',
"should display only one seen indicator icon"
);
});
QUnit.test('rendering when everyone have received the message', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" });
const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" });
const mailChannelId = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
[0, 0, { partner_id: resPartnerId2 }],
],
channel_type: 'chat',
});
const mailMessageId = pyEnv['mail.message'].create({
author_id: pyEnv.currentPartnerId,
body: "<p>Test</p>",
model: 'mail.channel',
res_id: mailChannelId,
});
const mailChannelMemberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]);
pyEnv['mail.channel.member'].write(mailChannelMemberIds, {
fetched_message_id: mailMessageId,
seen_message_id: false,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_MessageSeenIndicator',
"should display a message seen indicator component"
);
assert.doesNotHaveClass(
document.querySelector('.o_MessageSeenIndicator'),
'o-all-seen',
"indicator component should not be considered as all seen"
);
assert.containsOnce(
document.body,
'.o_MessageSeenIndicator_icon',
"should display only one seen indicator icon"
);
});
QUnit.test('rendering when just one has seen the message', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" });
const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" });
const mailChannelId = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
[0, 0, { partner_id: resPartnerId2 }],
],
channel_type: 'chat',
});
const mailMessageId = pyEnv['mail.message'].create({
author_id: pyEnv.currentPartnerId,
body: "<p>Test</p>",
model: 'mail.channel',
res_id: mailChannelId,
});
const mailChannelMemberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]);
pyEnv['mail.channel.member'].write(mailChannelMemberIds, {
fetched_message_id: mailMessageId,
seen_message_id: false,
});
const [mailChannelMemberId1] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId], ['partner_id', '=', resPartnerId1]]);
pyEnv['mail.channel.member'].write([mailChannelMemberId1], {
seen_message_id: mailMessageId,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_MessageSeenIndicator',
"should display a message seen indicator component"
);
assert.doesNotHaveClass(
document.querySelector('.o_MessageSeenIndicator'),
'o-all-seen',
"indicator component should not be considered as all seen"
);
assert.containsN(
document.body,
'.o_MessageSeenIndicator_icon',
2,
"should display two seen indicator icon"
);
});
QUnit.test('rendering when just one has seen & received the message', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" });
const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" });
const mailChannelId = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
[0, 0, { partner_id: resPartnerId2 }],
],
channel_type: 'chat',
});
const mailMessageId = pyEnv['mail.message'].create({
author_id: pyEnv.currentPartnerId,
body: "<p>Test</p>",
model: 'mail.channel',
res_id: mailChannelId,
});
const [mailChannelMemberId1] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId], ['partner_id', '=', resPartnerId1]]);
pyEnv['mail.channel.member'].write([mailChannelMemberId1], {
seen_message_id: mailMessageId,
fetched_message_id: mailMessageId,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_MessageSeenIndicator',
"should display a message seen indicator component"
);
assert.doesNotHaveClass(
document.querySelector('.o_MessageSeenIndicator'),
'o-all-seen',
"indicator component should not be considered as all seen"
);
assert.containsN(
document.body,
'.o_MessageSeenIndicator_icon',
2,
"should display two seen indicator icon"
);
});
QUnit.test('rendering when just everyone has seen the message', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" });
const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" });
const mailChannelId = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
[0, 0, { partner_id: resPartnerId2 }],
],
channel_type: 'chat',
});
const mailMessageId = pyEnv['mail.message'].create({
author_id: pyEnv.currentPartnerId,
body: "<p>Test</p>",
model: 'mail.channel',
res_id: mailChannelId,
});
const mailChannelMemberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]);
pyEnv['mail.channel.member'].write(mailChannelMemberIds, {
fetched_message_id: mailMessageId,
seen_message_id: mailMessageId,
});
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_MessageSeenIndicator',
"should display a message seen indicator component"
);
assert.hasClass(
document.querySelector('.o_MessageSeenIndicator'),
'o-all-seen',
"indicator component should not considered as all seen"
);
assert.containsN(
document.body,
'.o_MessageSeenIndicator_icon',
2,
"should display two seen indicator icon"
);
});
});
});

View file

@ -0,0 +1,913 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { browser } from '@web/core/browser/browser';
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { makeTestPromise } from 'web.test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('messaging_menu_tests.js');
QUnit.test('[technical] messaging not created then becomes created', async function (assert) {
/**
* Creation of messaging in env is async due to generation of models being
* async. Generation of models is async because it requires parsing of all
* JS modules that contain pieces of model definitions.
*
* Time of having no messaging is very short, almost imperceptible by user
* on UI, but the display should not crash during this critical time period.
*/
assert.expect(2);
const messagingBeforeCreationDeferred = makeTestPromise();
await start({
messagingBeforeCreationDeferred,
waitUntilMessagingCondition: 'none',
});
assert.containsOnce(
document.body,
'.o_MessagingMenuContainer_spinner',
"messaging menu container should have spinner when messaging is not yet created"
);
// simulate messaging becoming created
await afterNextRender(() => messagingBeforeCreationDeferred.resolve());
assert.containsOnce(
document.body,
'.o_MessagingMenu',
"messaging menu container should contain messaging menu after messaging has been created"
);
});
QUnit.test('messaging not initialized', async function (assert) {
assert.expect(2);
const { click } = await start({
async mockRPC(route) {
if (route === '/mail/init_messaging') {
// simulate messaging never initialized
return new Promise(resolve => {});
}
},
waitUntilMessagingCondition: 'created',
});
assert.strictEqual(
document.querySelectorAll('.o_MessagingMenu_loading').length,
1,
"should display loading icon on messaging menu when messaging not yet initialized"
);
await click(`.o_MessagingMenu_toggler`);
assert.strictEqual(
document.querySelector('.o_MessagingMenu_dropdownMenu').textContent,
"Please wait...",
"should prompt loading when opening messaging menu"
);
});
QUnit.test('messaging becomes initialized', async function (assert) {
assert.expect(2);
const messagingInitializedProm = makeTestPromise();
const { click } = await start({
async mockRPC(route) {
if (route === '/mail/init_messaging') {
await messagingInitializedProm;
}
},
waitUntilMessagingCondition: 'created',
});
await click(`.o_MessagingMenu_toggler`);
// simulate messaging becomes initialized
await afterNextRender(() => messagingInitializedProm.resolve());
assert.strictEqual(
document.querySelectorAll('.o_MessagingMenu_loading').length,
0,
"should no longer display loading icon on messaging menu when messaging becomes initialized"
);
assert.notOk(
document.querySelector('.o_MessagingMenu_dropdownMenu').textContent.includes("Please wait..."),
"should no longer prompt loading when opening messaging menu when messaging becomes initialized"
);
});
QUnit.test('basic rendering', async function (assert) {
assert.expect(21);
patchWithCleanup(browser, {
Notification: {
...browser.Notification,
permission: 'denied',
},
});
const { click } = await start();
assert.strictEqual(
document.querySelectorAll('.o_MessagingMenu').length,
1,
"should have messaging menu"
);
assert.notOk(
document.querySelector('.o_MessagingMenu').classList.contains('show'),
"should not mark messaging menu item as shown by default"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_toggler`).length,
1,
"should have clickable element on messaging menu"
);
assert.notOk(
document.querySelector(`.o_MessagingMenu_toggler`).classList.contains('show'),
"should not mark messaging menu clickable item as shown by default"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_icon`).length,
1,
"should have icon on clickable element in messaging menu"
);
assert.ok(
document.querySelector(`.o_MessagingMenu_icon`).classList.contains('fa-comments'),
"should have 'comments' icon on clickable element in messaging menu"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length,
0,
"should not display any messaging menu dropdown by default"
);
await click(`.o_MessagingMenu_toggler`);
assert.hasClass(
document.querySelector('.o_MessagingMenu'),
"show",
"should mark messaging menu as opened"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length,
1,
"should display messaging menu dropdown after click"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_dropdownMenuHeader`).length,
1,
"should have dropdown menu header"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenuHeader
.o_MessagingMenuTab
`).length,
3,
"should have 3 tab buttons to filter items in the header"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="all"]`).length,
1,
"1 tab button should be 'All'"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="chat"]`).length,
1,
"1 tab button should be 'Chat'"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="channel"]`).length,
1,
"1 tab button should be 'Channels'"
);
assert.ok(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should be active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should not be active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should not be active"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length,
1,
"should have button to make a new message"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_NotificationList
`).length,
1,
"should display thread preview list"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_NotificationList_noConversation
`).length,
1,
"should display no conversation in thread preview list"
);
await click(`.o_MessagingMenu_toggler`);
assert.doesNotHaveClass(
document.querySelector('.o_MessagingMenu'),
"show",
"should mark messaging menu as closed"
);
});
QUnit.test('counter is taking into account failure notification', async function (assert) {
assert.expect(2);
patchWithCleanup(browser, {
Notification: {
...browser.Notification,
permission: 'denied',
},
});
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
model: 'mail.channel',
res_id: mailChannelId1,
});
const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]);
pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 });
// failure that is expected to be used in the test
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1, // id of the related message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'email',
});
await start();
assert.containsOnce(
document.body,
'.o_MessagingMenu_counter',
"should display a notification counter next to the messaging menu for one notification"
);
assert.strictEqual(
document.querySelector('.o_MessagingMenu_counter').textContent,
"1",
"should display a counter of '1' next to the messaging menu"
);
});
QUnit.test('switch tab', async function (assert) {
assert.expect(15);
const { click } = await start();
await click(`.o_MessagingMenu_toggler`);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="all"]`).length,
1,
"1 tab button should be 'All'"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="chat"]`).length,
1,
"1 tab button should be 'Chat'"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="channel"]`).length,
1,
"1 tab button should be 'Channels'"
);
assert.ok(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should be active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should not be active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should not be active"
);
await click(`.o_MessagingMenuTab[data-tab-id="chat"]`);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should become inactive"
);
assert.ok(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should not become active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should stay inactive"
);
await click(`.o_MessagingMenuTab[data-tab-id="channel"]`);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should stay active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should become inactive"
);
assert.ok(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should become active"
);
await click(`.o_MessagingMenuTab[data-tab-id="all"]`);
assert.ok(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should become active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should stay inactive"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should become inactive"
);
});
QUnit.test('new message', async function (assert) {
assert.expect(3);
const { click } = await start();
await click(`.o_MessagingMenu_toggler`);
await click(`.o_MessagingMenu_newMessageButton`);
assert.strictEqual(
document.querySelectorAll(`.o_ChatWindow`).length,
1,
"should have open a chat window"
);
assert.ok(
document.querySelector(`.o_ChatWindow`).classList.contains('o-new-message'),
"chat window should be for new message"
);
assert.ok(
document.querySelector(`.o_ChatWindow`).classList.contains('o-focused'),
"chat window should be focused"
);
});
QUnit.test('no new message when discuss is open', async function (assert) {
assert.expect(3);
const { click, openDiscuss, openView } = await start();
await openDiscuss({ waitUntilMessagesLoaded: false });
await click(`.o_MessagingMenu_toggler`);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length,
0,
"should not have 'new message' when discuss is open"
);
await openView({
res_model: 'res.partner',
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length,
1,
"should have 'new message' when discuss is closed"
);
await openDiscuss({ waitUntilMessagesLoaded: false });
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length,
0,
"should not have 'new message' when discuss is open again"
);
});
QUnit.test('channel preview: basic rendering', async function (assert) {
assert.expect(9);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" });
const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" });
pyEnv['mail.message'].create({
author_id: resPartnerId1, // not current partner, will be asserted in the test
body: "<p>test</p>", // random body, will be asserted in the test
model: 'mail.channel', // necessary to link message to channel
res_id: mailChannelId1, // id of related channel
});
const { click } = await start();
await click(`.o_MessagingMenu_toggler`);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView
`).length,
1,
"should have one preview"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView_sidebar
`).length,
1,
"preview should have a sidebar"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView_content
`).length,
1,
"preview should have some content"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView_header
`).length,
1,
"preview should have header in content"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView_header
.o_ChannelPreviewView_name
`).length,
1,
"preview should have name in header of content"
);
assert.strictEqual(
document.querySelector(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView_name
`).textContent,
"General", "preview should have name of channel"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView_content
.o_ChannelPreviewView_core
`).length,
1,
"preview should have core in content"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView_core
.o_ChannelPreviewView_inlineText
`).length,
1,
"preview should have inline text in core of content"
);
assert.strictEqual(
document.querySelector(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView_core
.o_ChannelPreviewView_inlineText
`).textContent.trim(),
"Demo: test",
"preview should have message content as inline text of core content"
);
});
QUnit.test('filtered previews', async function (assert) {
assert.expect(12);
const pyEnv = await startServer();
const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([
{ channel_type: "chat" },
{ name: "mailChannel1" },
]);
pyEnv['mail.message'].create([
{
model: 'mail.channel', // to link message to channel
res_id: mailChannelId1, // id of related channel
},
{
model: 'mail.channel', // to link message to channel
res_id: mailChannelId2, // id of related channel
},
]);
const { click } = await start();
await click(`.o_MessagingMenu_toggler`);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`).length,
2,
"should have 2 previews"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]
`).length,
1,
"should have preview of chat"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]
`).length,
1,
"should have preview of channel"
);
await click('.o_MessagingMenuTab[data-tab-id="chat"]');
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`).length,
1,
"should have one preview"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]
`).length,
1,
"should have preview of chat"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]
`).length,
0,
"should not have preview of channel"
);
await click('.o_MessagingMenuTab[data-tab-id="channel"]');
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView
`).length,
1,
"should have one preview"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]
`).length,
0,
"should not have preview of chat"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]
`).length,
1,
"should have preview of channel"
);
await click('.o_MessagingMenuTab[data-tab-id="all"]');
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`).length,
2,
"should have 2 previews"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]
`).length,
1,
"should have preview of chat"
);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]
`).length,
1,
"should have preview of channel"
);
});
QUnit.test('open chat window from preview', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({});
const { click } = await start();
await click(`.o_MessagingMenu_toggler`);
await click(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`);
assert.strictEqual(
document.querySelectorAll(`.o_ChatWindow`).length,
1,
"should have open a chat window"
);
});
QUnit.test('no code injection in message body preview', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: "<p><em>&shoulnotberaised</em><script>throw new Error('CodeInjectionError');</script></p>",
model: "mail.channel",
res_id: mailChannelId1,
});
const { click } = await start();
await click(`.o_MessagingMenu_toggler`);
assert.containsOnce(
document.body,
'.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView',
"should display a preview",
);
assert.containsOnce(
document.body,
'.o_ChannelPreviewView_core',
"preview should have core in content",
);
assert.containsOnce(
document.body,
'.o_ChannelPreviewView_inlineText',
"preview should have inline text in core of content",
);
assert.strictEqual(
document.querySelector('.o_ChannelPreviewView_inlineText')
.textContent.replace(/\s/g, ""),
"You:&shoulnotberaisedthrownewError('CodeInjectionError');",
"should display correct uninjected last message inline content"
);
assert.containsNone(
document.querySelector('.o_ChannelPreviewView_inlineText'),
'script',
"last message inline content should not have any code injection"
);
});
QUnit.test('no code injection in message body preview from sanitized message', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: "<p>&lt;em&gt;&shoulnotberaised&lt;/em&gt;&lt;script&gt;throw new Error('CodeInjectionError');&lt;/script&gt;</p>",
model: "mail.channel",
res_id: mailChannelId1,
});
const { click } = await start();
await click(`.o_MessagingMenu_toggler`);
assert.containsOnce(
document.body,
'.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView',
"should display a preview",
);
assert.containsOnce(
document.body,
'.o_ChannelPreviewView_core',
"preview should have core in content",
);
assert.containsOnce(
document.body,
'.o_ChannelPreviewView_inlineText',
"preview should have inline text in core of content",
);
assert.strictEqual(
document.querySelector('.o_ChannelPreviewView_inlineText')
.textContent.replace(/\s/g, ""),
"You:<em>&shoulnotberaised</em><script>thrownewError('CodeInjectionError');</script>",
"should display correct uninjected last message inline content"
);
assert.containsNone(
document.querySelector('.o_ChannelPreviewView_inlineText'),
'script',
"last message inline content should not have any code injection"
);
});
QUnit.test('<br/> tags in message body preview are transformed in spaces', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
body: "<p>a<br/>b<br>c<br />d<br ></p>",
model: "mail.channel",
res_id: mailChannelId1,
});
const { click } = await start();
await click(`.o_MessagingMenu_toggler`);
assert.containsOnce(
document.body,
'.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView',
"should display a preview",
);
assert.containsOnce(
document.body,
'.o_ChannelPreviewView_core',
"preview should have core in content",
);
assert.containsOnce(
document.body,
'.o_ChannelPreviewView_inlineText',
"preview should have inline text in core of content",
);
assert.strictEqual(
document.querySelector('.o_ChannelPreviewView_inlineText').textContent,
"You: a b c d",
"should display correct last message inline content with brs replaced by spaces"
);
});
QUnit.test('rendering with OdooBot has a request (default)', async function (assert) {
assert.expect(4);
patchWithCleanup(browser, {
Notification: {
...browser.Notification,
permission: 'default',
},
});
const { click } = await start();
assert.ok(
document.querySelector('.o_MessagingMenu_counter'),
"should display a notification counter next to the messaging menu for OdooBot request"
);
assert.strictEqual(
document.querySelector('.o_MessagingMenu_counter').textContent,
"1",
"should display a counter of '1' next to the messaging menu"
);
await click('.o_MessagingMenu_toggler');
assert.containsOnce(
document.body,
'.o_NotificationRequest',
"should display a notification in the messaging menu"
);
assert.strictEqual(
document.querySelector('.o_NotificationRequest_name').textContent.trim(),
'OdooBot has a request',
"notification should display that OdooBot has a request"
);
});
QUnit.test('rendering without OdooBot has a request (denied)', async function (assert) {
assert.expect(2);
patchWithCleanup(browser, {
Notification: {
permission: 'denied',
},
});
const { click } = await start();
assert.containsNone(
document.body,
'.o_MessagingMenu_counter',
"should not display a notification counter next to the messaging menu"
);
await click('.o_MessagingMenu_toggler');
assert.containsNone(
document.body,
'.o_NotificationRequest',
"should display no notification in the messaging menu"
);
});
QUnit.test('rendering without OdooBot has a request (accepted)', async function (assert) {
assert.expect(2);
patchWithCleanup(browser, {
Notification: {
permission: 'granted',
},
});
const { click } = await start();
assert.containsNone(
document.body,
'.o_MessagingMenu_counter',
"should not display a notification counter next to the messaging menu"
);
await click('.o_MessagingMenu_toggler');
assert.containsNone(
document.body,
'.o_NotificationRequest',
"should display no notification in the messaging menu"
);
});
QUnit.test('respond to notification prompt (denied)', async function (assert) {
assert.expect(4);
patchWithCleanup(browser, {
Notification: {
permission: 'default',
async requestPermission() {
this.permission = 'denied';
return this.permission;
},
},
});
const { click } = await start({
services: {
notification: makeFakeNotificationService(() => {
assert.step(
"should display a toast notification with the deny confirmation"
);
}),
},
});
await click('.o_MessagingMenu_toggler');
await click('.o_NotificationRequest');
assert.verifySteps([
"should display a toast notification with the deny confirmation",
]);
assert.containsNone(
document.body,
'.o_MessagingMenu_counter',
"should not display a notification counter next to the messaging menu"
);
await click('.o_MessagingMenu_toggler');
assert.containsNone(
document.body,
'.o_NotificationRequest',
"should display no notification in the messaging menu"
);
});
QUnit.test('Group chat should be displayed inside the chat section of the messaging menu', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_type: 'group',
});
const { click } = await start();
await click('.o_MessagingMenu_toggler');
await click(`.o_MessagingMenuTab[data-tab-id="chat"]`);
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
.o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]
`).length,
1,
"should have one preview of group"
);
});
});
});

View file

@ -0,0 +1,484 @@
/** @odoo-module **/
import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
import { click as clickContains, contains } from '@web/../tests/utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('notification_list_notification_group_tests.js');
QUnit.test('notification group basic layout', async function (assert) {
assert.expect(10);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
message_type: 'email', // message must be email (goal of the test)
model: 'mail.channel', // expected value to link message to channel
res_id: mailChannelId1,
res_model_name: "Channel", // random res model name, will be asserted in the test
});
pyEnv['mail.notification'].create([
{
mail_message_id: mailMessageId1,
notification_status: 'exception',
notification_type: 'email',
},
{
mail_message_id: mailMessageId1,
notification_status: 'exception',
notification_type: 'email',
},
]);
const { click } = await start();
await click('.o_MessagingMenu_toggler');
assert.containsOnce(
document.body,
'.o_NotificationGroup',
"should have 1 notification group"
);
assert.containsOnce(
document.body,
'.o_NotificationGroup_name',
"should have 1 group name"
);
assert.strictEqual(
document.querySelector('.o_NotificationGroup_name').textContent,
"Channel",
"should have model name as group name"
);
assert.containsOnce(
document.body,
'.o_NotificationGroup_counter',
"should have 1 group counter"
);
assert.strictEqual(
document.querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in the group"
);
assert.containsOnce(
document.body,
'.o_NotificationGroup_date',
"should have 1 group date"
);
assert.strictEqual(
document.querySelector('.o_NotificationGroup_date').textContent,
"a few seconds ago",
"should have the group date corresponding to now"
);
assert.containsOnce(
document.body,
'.o_NotificationGroup_inlineText',
"should have 1 group text"
);
assert.strictEqual(
document.querySelector('.o_NotificationGroup_inlineText').textContent.trim(),
"An error occurred when sending an email.",
"should have the group text corresponding to email"
);
assert.containsOnce(
document.body,
'.o_NotificationGroup_markAsRead',
"should have 1 mark as read button"
);
});
QUnit.test('mark as read', async function (assert) {
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
message_type: 'email', // message must be email (goal of the test)
model: 'mail.channel', // expected value to link message to channel
res_id: mailChannelId1,
res_model_name: "Channel", // random res model name, will be asserted in the test
});
// failure that is expected to be used in the test
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1, // id of the related message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'email',
});
await start();
await clickContains('.o_MessagingMenu_toggler');
await contains('.o_NotificationGroup');
await clickContains('.o_NotificationGroup_markAsRead');
await contains('.o_NotificationGroup', { count: 0 });
});
QUnit.test('grouped notifications by document', async function (assert) {
// If some failures linked to a document refers to a same document, a single
// notification should group all those failures.
assert.expect(5);
const pyEnv = await startServer();
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
// first message that is expected to have a failure
{
message_type: 'email', // message must be email (goal of the test)
model: 'res.partner', // same model as second message (and not `mail.channel`)
res_id: 31, // same res_id as second message
res_model_name: "Partner", // random related model name
},
// second message that is expected to have a failure
{
message_type: 'email', // message must be email (goal of the test)
model: 'res.partner', // same model as first message (and not `mail.channel`)
res_id: 31, // same res_id as first message
res_model_name: "Partner", // same related model name for consistency
},
]);
pyEnv['mail.notification'].create([
// first failure that is expected to be used in the test
{
mail_message_id: mailMessageId1, // id of the related first message
notification_status: 'exception', // one possible value to have a failure
notification_type: 'email', // expected failure type for email message
},
// second failure that is expected to be used in the test
{
mail_message_id: mailMessageId2, // id of the related second message
notification_status: 'bounce', // other possible value to have a failure
notification_type: 'email', // expected failure type for email message
}
]);
const { click } = await start();
await click('.o_MessagingMenu_toggler');
assert.containsOnce(
document.body,
'.o_NotificationGroup',
"should have 1 notification group"
);
assert.containsOnce(
document.body,
'.o_NotificationGroup_counter',
"should have 1 group counter"
);
assert.strictEqual(
document.querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in the group"
);
assert.containsNone(
document.body,
'.o_ChatWindow',
"should have no chat window initially"
);
await click('.o_NotificationGroup');
assert.containsOnce(
document.body,
'.o_ChatWindow',
"should have opened the thread in a chat window after clicking on it"
);
});
QUnit.test('grouped notifications by document model', async function (assert) {
// If all failures linked to a document model refers to different documents,
// a single notification should group all failures that are linked to this
// document model.
assert.expect(12);
const pyEnv = await startServer();
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
// first message that is expected to have a failure
{
message_type: 'email', // message must be email (goal of the test)
model: 'res.partner', // same model as second message (and not `mail.channel`)
res_id: 31, // different res_id from second message
res_model_name: "Partner", // random related model name
},
// second message that is expected to have a failure
{
message_type: 'email', // message must be email (goal of the test)
model: 'res.partner', // same model as first message (and not `mail.channel`)
res_id: 32, // different res_id from first message
res_model_name: "Partner", // same related model name for consistency
},
]);
pyEnv['mail.notification'].create([
// first failure that is expected to be used in the test
{
mail_message_id: mailMessageId1, // id of the related first message
notification_status: 'exception', // one possible value to have a failure
notification_type: 'email', // expected failure type for email message
},
// second failure that is expected to be used in the test
{
mail_message_id: mailMessageId2, // id of the related second message
notification_status: 'bounce', // other possible value to have a failure
notification_type: 'email', // expected failure type for email message
},
]);
const { click, env } = await start();
patchWithCleanup(env.services.action, {
doAction(action) {
assert.step('do_action');
assert.strictEqual(
action.name,
"Mail Failures",
"action should have 'Mail Failures' as name",
);
assert.strictEqual(
action.type,
'ir.actions.act_window',
"action should have the type act_window"
);
assert.strictEqual(
action.view_mode,
'kanban,list,form',
"action should have 'kanban,list,form' as view_mode"
);
assert.strictEqual(
JSON.stringify(action.views),
JSON.stringify([[false, 'kanban'], [false, 'list'], [false, 'form']]),
"action should have correct views"
);
assert.strictEqual(
action.target,
'current',
"action should have 'current' as target"
);
assert.strictEqual(
action.res_model,
'res.partner',
"action should have the group model as res_model"
);
assert.strictEqual(
JSON.stringify(action.domain),
JSON.stringify([['message_has_error', '=', true]]),
"action should have 'message_has_error' as domain"
);
},
});
await click('.o_MessagingMenu_toggler');
assert.containsOnce(
document.body,
'.o_NotificationGroup',
"should have 1 notification group"
);
assert.containsOnce(
document.body,
'.o_NotificationGroup_counter',
"should have 1 group counter"
);
assert.strictEqual(
document.querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in the group"
);
document.querySelector('.o_NotificationGroup').click();
assert.verifySteps(
['do_action'],
"should do an action to display the related records"
);
});
QUnit.test('different mail.channel are not grouped', async function (assert) {
// `mail.channel` is a special case where notifications are not grouped when
// they are linked to different channels, even though the model is the same.
assert.expect(6);
const pyEnv = await startServer();
const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: "mailChannel1" }, { name: "mailChannel2" }]);
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
// first message that is expected to have a failure
{
message_type: 'email', // message must be email (goal of the test)
model: 'mail.channel', // testing a channel is the goal of the test
res_id: mailChannelId1, // different res_id from second message
res_model_name: "Channel", // random related model name
},
// second message that is expected to have a failure
{
message_type: 'email', // message must be email (goal of the test)
model: 'mail.channel', // testing a channel is the goal of the test
res_id: mailChannelId2, // different res_id from first message
res_model_name: "Channel", // same related model name for consistency
},
]);
pyEnv['mail.notification'].create([
{
mail_message_id: mailMessageId1, // id of the related first message
notification_status: 'exception', // one possible value to have a failure
notification_type: 'email', // expected failure type for email message
},
{
mail_message_id: mailMessageId1,
notification_status: 'exception',
notification_type: 'email',
},
{
mail_message_id: mailMessageId2, // id of the related second message
notification_status: 'bounce', // other possible value to have a failure
notification_type: 'email', // expected failure type for email message
},
{
mail_message_id: mailMessageId2,
notification_status: 'bounce',
notification_type: 'email',
},
]);
const { click } = await start();
await click('.o_MessagingMenu_toggler');
assert.containsN(
document.body,
'.o_NotificationGroup',
2,
"should have 2 notifications group"
);
const groups = document.querySelectorAll('.o_NotificationGroup');
assert.containsOnce(
groups[0],
'.o_NotificationGroup_counter',
"should have 1 group counter in first group"
);
assert.strictEqual(
groups[0].querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in first group"
);
assert.containsOnce(
groups[1],
'.o_NotificationGroup_counter',
"should have 1 group counter in second group"
);
assert.strictEqual(
groups[1].querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in second group"
);
await afterNextRender(() => groups[0].click());
assert.containsOnce(
document.body,
'.o_ChatWindow',
"should have opened the channel related to the first group in a chat window"
);
});
QUnit.test('multiple grouped notifications by document model, sorted by the most recent message of each group', async function (assert) {
assert.expect(9);
const pyEnv = await startServer();
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
// first message that is expected to have a failure
{
message_type: 'email', // message must be email (goal of the test)
model: 'res.partner', // different model from second message
res_id: 31,
res_model_name: "Partner", // random related model name
},
// second message that is expected to have a failure
{
message_type: 'email', // message must be email (goal of the test)
model: 'res.company', // different model from first message
res_id: 32,
res_model_name: "Company", // random related model name
},
]);
pyEnv['mail.notification'].create([
{
mail_message_id: mailMessageId1, // id of the related first message
notification_status: 'exception', // one possible value to have a failure
notification_type: 'email', // expected failure type for email message
},
{
mail_message_id: mailMessageId1,
notification_status: 'exception',
notification_type: 'email',
},
{
mail_message_id: mailMessageId2, // id of the related second message
notification_status: 'bounce', // other possible value to have a failure
notification_type: 'email', // expected failure type for email message
},
{
mail_message_id: mailMessageId2,
notification_status: 'bounce',
notification_type: 'email',
},
]);
const { click } = await start();
await click('.o_MessagingMenu_toggler');
assert.containsN(
document.body,
'.o_NotificationGroup',
2,
"should have 2 notifications group"
);
const groups = document.querySelectorAll('.o_NotificationGroup');
assert.containsOnce(
groups[0],
'.o_NotificationGroup_name',
"should have 1 group name in first group"
);
assert.strictEqual(
groups[0].querySelector('.o_NotificationGroup_name').textContent,
"Company",
"should have first model name as group name"
);
assert.containsOnce(
groups[0],
'.o_NotificationGroup_counter',
"should have 1 group counter in first group"
);
assert.strictEqual(
groups[0].querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in first group"
);
assert.containsOnce(
groups[1],
'.o_NotificationGroup_name',
"should have 1 group name in second group"
);
assert.strictEqual(
groups[1].querySelector('.o_NotificationGroup_name').textContent,
"Partner",
"should have second model name as group name"
);
assert.containsOnce(
groups[1],
'.o_NotificationGroup_counter',
"should have 1 group counter in second group"
);
assert.strictEqual(
groups[1].querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in second group"
);
});
QUnit.test('non-failure notifications are ignored', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
message_type: 'email', // message must be email (goal of the test)
model: 'res.partner', // random model
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1, // id of the related first message
notification_status: 'ready', // non-failure status
notification_type: 'email', // expected notification type for email message
});
const { click } = await start();
await click('.o_MessagingMenu_toggler');
assert.containsNone(
document.body,
'.o_NotificationGroup',
"should have 0 notification group"
);
});
});
});

View file

@ -0,0 +1,115 @@
/** @odoo-module **/
import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('notification_list_tests.js');
QUnit.test('marked as read thread notifications are ordered by last message date', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([
{ name: "Channel 2019" },
{ name: "Channel 2020" },
]);
pyEnv['mail.message'].create([
{
date: "2019-01-01 00:00:00",
model: 'mail.channel',
res_id: mailChannelId1,
},
{
date: "2020-01-01 00:00:00",
model: 'mail.channel',
res_id: mailChannelId2,
},
]);
const { click } = await start();
await click('.o_MessagingMenu_toggler');
assert.containsN(
document.body,
'.o_ChannelPreviewView',
2,
"there should be two thread previews"
);
const channelPreviewViewElList = document.querySelectorAll('.o_ChannelPreviewView');
assert.strictEqual(
channelPreviewViewElList[0].querySelector(':scope .o_ChannelPreviewView_name').textContent,
'Channel 2020',
"First channel in the list should be the channel of 2020 (more recent last message)"
);
assert.strictEqual(
channelPreviewViewElList[1].querySelector(':scope .o_ChannelPreviewView_name').textContent,
'Channel 2019',
"Second channel in the list should be the channel of 2019 (least recent last message)"
);
});
QUnit.test('thread notifications are re-ordered on receiving a new message', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([
{ name: "Channel 2019" },
{ name: "Channel 2020" },
]);
pyEnv['mail.message'].create([
{
date: "2019-01-01 00:00:00",
model: 'mail.channel',
res_id: mailChannelId1,
},
{
date: "2020-01-01 00:00:00",
model: 'mail.channel',
res_id: mailChannelId2,
},
]);
const { click } = await start();
await click('.o_MessagingMenu_toggler');
assert.containsN(
document.body,
'.o_ChannelPreviewView',
2,
"there should be two thread previews"
);
const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0];
await afterNextRender(() => {
pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel/new_message', {
'id': mailChannelId1,
'message': {
author_id: [7, "Demo User"],
body: "<p>New message !</p>",
date: "2020-03-23 10:00:00",
id: 44,
message_type: 'comment',
model: 'mail.channel',
record_name: 'Channel 2019',
res_id: mailChannelId1,
},
});
});
assert.containsN(
document.body,
'.o_ChannelPreviewView',
2,
"there should still be two thread previews"
);
const channelPreviewViewElList = document.querySelectorAll('.o_ChannelPreviewView');
assert.strictEqual(
channelPreviewViewElList[0].querySelector(':scope .o_ChannelPreviewView_name').textContent,
'Channel 2019',
"First channel in the list should now be 'Channel 2019'"
);
assert.strictEqual(
channelPreviewViewElList[1].querySelector(':scope .o_ChannelPreviewView_name').textContent,
'Channel 2020',
"Second channel in the list should now be 'Channel 2020'"
);
});
});
});

View file

@ -0,0 +1,191 @@
/** @odoo-module **/
import { UPDATE_BUS_PRESENCE_DELAY } from '@bus/im_status_service';
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { contains } from "@web/../tests/utils";
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('persona_im_status_icon_tests.js');
QUnit.test('initially online', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const partnerId = pyEnv['res.partner'].create({ im_status: 'online' });
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
author_id: partnerId,
body: 'not empty',
model: 'mail.channel',
res_id: mailChannelId,
});
const { advanceTime, afterNextRender, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId,
},
},
hasTimeControl: true,
});
await openDiscuss();
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.strictEqual(
document.querySelectorAll(`.o_PersonaImStatusIcon.o-online`).length,
1,
"persona IM status icon should have online status rendering"
);
});
QUnit.test('initially offline', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const partnerId = pyEnv['res.partner'].create({ im_status: 'offline' });
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
author_id: partnerId,
body: 'not empty',
model: 'mail.channel',
res_id: mailChannelId,
});
const { advanceTime, afterNextRender, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId,
},
},
hasTimeControl: true,
});
await openDiscuss();
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.strictEqual(
document.querySelectorAll(`.o_PersonaImStatusIcon.o-offline`).length,
1,
"persona IM status icon should have offline status rendering"
);
});
QUnit.test('initially away', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const partnerId = pyEnv['res.partner'].create({ im_status: 'away' });
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
author_id: partnerId,
body: 'not empty',
model: 'mail.channel',
res_id: mailChannelId,
});
const { advanceTime, afterNextRender, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId,
},
},
hasTimeControl: true,
});
await openDiscuss();
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.strictEqual(
document.querySelectorAll(`.o_PersonaImStatusIcon.o-away`).length,
1,
"persona IM status icon should have away status rendering"
);
});
QUnit.test('change icon on change partner im_status', async function (assert) {
const pyEnv = await startServer();
const partnerId = pyEnv['res.partner'].create({ im_status: 'online' });
const mailChannelId = pyEnv['mail.channel'].create({});
pyEnv['mail.message'].create({
author_id: partnerId,
body: 'not empty',
model: 'mail.channel',
res_id: mailChannelId,
});
const { advanceTime, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId,
},
},
hasTimeControl: true,
});
await openDiscuss();
await advanceTime(UPDATE_BUS_PRESENCE_DELAY);
await contains(".o_PersonaImStatusIcon.o-online");
pyEnv["res.partner"].write([partnerId], { im_status: "offline" });
await advanceTime(UPDATE_BUS_PRESENCE_DELAY);
await contains(".o_PersonaImStatusIcon.o-offline");
pyEnv["res.partner"].write([partnerId], { im_status: "away" });
await advanceTime(UPDATE_BUS_PRESENCE_DELAY);
await contains(".o_PersonaImStatusIcon.o-away");
pyEnv["res.partner"].write([partnerId], { im_status: "online" });
await advanceTime(UPDATE_BUS_PRESENCE_DELAY);
await contains(".o_PersonaImStatusIcon.o-online");
});
QUnit.test('change icon on change guest im_status', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const guestId = pyEnv['mail.guest'].create({ im_status: 'online' });
const mailChannelId = pyEnv['mail.channel'].create({
channel_member_ids: [[0, 0, { partner_id: pyEnv.currentPartnerId }], [0, 0, { guest_id: guestId }]],
channel_type: 'group',
});
pyEnv['mail.message'].create({
author_guest_id: guestId,
author_id: false,
body: 'not empty',
model: 'mail.channel',
res_id: mailChannelId,
});
const { advanceTime, afterNextRender, openDiscuss } = await start({
discuss: {
params: {
default_active_id: mailChannelId,
},
},
hasTimeControl: true,
});
await openDiscuss();
assert.strictEqual(
document.querySelectorAll(`.o_PersonaImStatusIcon.o-online`).length,
1,
"persona IM status icon should have online status rendering"
);
pyEnv['mail.guest'].write([guestId], { im_status: 'offline' });
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.strictEqual(
document.querySelectorAll(`.o_PersonaImStatusIcon.o-offline`).length,
1,
"persona IM status icon should have offline status rendering"
);
pyEnv['mail.guest'].write([guestId], { im_status: 'away' });
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.strictEqual(
document.querySelectorAll(`.o_PersonaImStatusIcon.o-away`).length,
1,
"persona IM status icon should have away status rendering"
);
pyEnv['mail.guest'].write([guestId], { im_status: 'online' });
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.strictEqual(
document.querySelectorAll(`.o_PersonaImStatusIcon.o-online`).length,
1,
"persona IM status icon should have online status rendering in the end"
);
});
});
});

View file

@ -0,0 +1,83 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('thread_icon_tests.js');
QUnit.test('chat: correspondent is typing', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({
im_status: 'online',
name: 'Demo',
});
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'chat',
});
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body.querySelector('.o_DiscussSidebarCategoryItem'),
'.o_ThreadIcon',
"should have thread icon in the sidebar"
);
assert.containsOnce(
document.body,
'.o_ThreadIcon_online',
"should have thread icon with persona IM status icon 'online'"
);
// simulate receive typing notification from demo "is typing"
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': true,
},
}));
assert.containsOnce(
document.body,
'.o_ThreadIcon_typing',
"should have thread icon with partner currently typing"
);
assert.strictEqual(
document.querySelector('.o_ThreadIcon_typing').title,
"Demo is typing...",
"title of icon should tell demo is currently typing"
);
// simulate receive typing notification from demo "no longer is typing"
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': false,
},
}));
assert.containsOnce(
document.body,
'.o_ThreadIcon_online',
"should have thread icon with persona IM status icon 'online' (no longer typing)"
);
});
});
});

View file

@ -0,0 +1,337 @@
/** @odoo-module **/
import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('thread_needaction_preview_tests.js');
QUnit.test('mark as read', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, messaging } = await start({
async mockRPC(route, args) {
if (route.includes('mark_all_as_read')) {
assert.step('mark_all_as_read');
assert.deepEqual(
args.kwargs.domain,
[
['model', '=', 'res.partner'],
['res_id', '=', resPartnerId1],
],
"should mark all as read the correct thread"
);
}
},
});
await afterNextRender(() => afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
message: "should wait until inbox loaded initial needaction messages",
predicate: ({ threadCache }) => {
return threadCache.thread === messaging.inbox.thread;
},
}));
assert.containsOnce(
document.body,
'.o_ThreadNeedactionPreview_markAsRead',
"should have 1 mark as read button"
);
await click('.o_ThreadNeedactionPreview_markAsRead');
assert.verifySteps(
['mark_all_as_read'],
"should have marked the thread as read"
);
assert.containsNone(
document.body,
'.o_ChatWindow',
"should not have opened the thread"
);
});
QUnit.test('click on preview should mark as read and open the thread', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, messaging } = await start();
await afterNextRender(() => afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
message: "should wait until inbox loaded initial needaction messages",
predicate: ({ threadCache }) => {
return threadCache.thread === messaging.inbox.thread;
},
}));
assert.containsOnce(
document.body,
'.o_ThreadNeedactionPreview',
"should have a preview initially"
);
assert.containsNone(
document.body,
'.o_ChatWindow',
"should have no chat window initially"
);
await click('.o_ThreadNeedactionPreview');
assert.containsOnce(
document.body,
'.o_ChatWindow',
"should have opened the thread on clicking on the preview"
);
await click('.o_MessagingMenu_toggler');
assert.containsNone(
document.body,
'.o_ThreadNeedactionPreview',
"should have no preview because the message should be marked as read after opening its thread"
);
});
QUnit.test('click on expand from chat window should close the chat window and open the form view', async function (assert) {
assert.expect(8);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, env, messaging } = await start();
patchWithCleanup(env.services.action, {
doAction(action) {
assert.step('do_action');
assert.strictEqual(
action.res_id,
resPartnerId1,
"should redirect to the id of the thread"
);
assert.strictEqual(
action.res_model,
'res.partner',
"should redirect to the model of the thread"
);
},
});
await afterNextRender(() => afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
message: "should wait until inbox loaded initial needaction messages",
predicate: ({ threadCache }) => {
return threadCache.thread === messaging.inbox.thread;
},
}));
assert.containsOnce(
document.body,
'.o_ThreadNeedactionPreview',
"should have a preview initially"
);
await click('.o_ThreadNeedactionPreview');
assert.containsOnce(
document.body,
'.o_ChatWindow',
"should have opened the thread on clicking on the preview"
);
assert.containsOnce(
document.body,
'.o_ChatWindowHeader_commandExpand',
"should have an expand button"
);
await click('.o_ChatWindowHeader_commandExpand');
assert.containsNone(
document.body,
'.o_ChatWindow',
"should have closed the chat window on clicking expand"
);
assert.verifySteps(
['do_action'],
"should have done an action to open the form view"
);
});
QUnit.test('[technical] opening a non-channel chat window should not call channel_fold', async function (assert) {
// channel_fold should not be called when opening non-channels in chat
// window, because there is no server sync of fold state for them.
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, messaging } = await start({
async mockRPC(route, args) {
if (route.includes('channel_fold')) {
const message = "should not call channel_fold when opening a non-channel chat window";
assert.step(message);
console.error(message);
throw Error(message);
}
},
});
await afterNextRender(() => afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
message: "should wait until inbox loaded initial needaction messages",
predicate: ({ threadCache }) => {
return threadCache.thread === messaging.inbox.thread;
},
}));
assert.containsOnce(
document.body,
'.o_ThreadNeedactionPreview',
"should have a preview initially"
);
assert.containsNone(
document.body,
'.o_ChatWindow',
"should have no chat window initially"
);
await click('.o_ThreadNeedactionPreview');
assert.containsOnce(
document.body,
'.o_ChatWindow',
"should have opened the chat window on clicking on the preview"
);
});
QUnit.test('preview should display last needaction message preview even if there is a more recent message that is not needaction in the thread', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({
name: "Stranger",
});
const mailMessageId1 = pyEnv['mail.message'].create({
author_id: resPartnerId1,
body: "I am the oldest but needaction",
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.message'].create({
author_id: pyEnv.currentPartnerId,
body: "I am more recent",
model: 'res.partner',
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, messaging } = await start();
await afterNextRender(() => afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
message: "should wait until inbox loaded initial needaction messages",
predicate: ({ threadCache }) => {
return threadCache.thread === messaging.inbox.thread;
},
}));
assert.containsOnce(
document.body,
'.o_ThreadNeedactionPreview_inlineText',
"should have a preview from the last message"
);
assert.strictEqual(
document.querySelector('.o_ThreadNeedactionPreview_inlineText').textContent,
'Stranger: I am the oldest but needaction',
"the displayed message should be the one that needs action even if there is a more recent message that is not needaction on the thread"
);
});
QUnit.test('chat window header should not have unread counter for non-channel thread', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailMessageId1 = pyEnv['mail.message'].create({
author_id: resPartnerId1,
body: 'not empty',
model: 'res.partner',
needaction: true,
needaction_partner_ids: [pyEnv.currentPartnerId],
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: pyEnv.currentPartnerId,
});
const { afterEvent, click, messaging } = await start();
await afterNextRender(() => afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
message: "should wait until inbox loaded initial needaction messages",
predicate: ({ threadCache }) => {
return threadCache.thread === messaging.inbox.thread;
},
}));
await click('.o_ThreadNeedactionPreview');
assert.containsOnce(
document.body,
'.o_ChatWindow',
"should have opened the chat window on clicking on the preview"
);
assert.containsNone(
document.body,
'.o_ChatWindowHeader_counter',
"chat window header should not have unread counter for non-channel thread"
);
});
});
});

View file

@ -0,0 +1,343 @@
/** @odoo-module **/
import {
afterNextRender,
nextAnimationFrame,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { contains } from "@web/../tests/utils";
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('thread_textual_typing_status_tests.js');
QUnit.test('receive other member typing status "is typing"', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
});
const { messaging, openDiscuss } = await start({
discuss: {
context: { active_id: mailChannelId1 },
},
});
await openDiscuss();
await contains(".o_ThreadTextualTypingStatus", { text: "" });
// simulate receive typing notification from demo
messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': true,
},
});
await contains(".o_ThreadTextualTypingStatus", { text: "Demo is typing..." });
});
QUnit.test('receive other member typing status "is typing" then "no longer is typing"', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
});
const { messaging, openDiscuss } = await start({
discuss: {
context: { active_id: mailChannelId1 },
},
});
await openDiscuss();
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"",
"Should display no one is currently typing"
);
// simulate receive typing notification from demo "is typing"
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': true,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Demo is typing...",
"Should display that demo user is typing"
);
// simulate receive typing notification from demo "is no longer typing"
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': false,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"",
"Should no longer display that demo user is typing"
);
});
QUnit.test('assume other member typing status becomes "no longer is typing" after 60 seconds without any updated typing status', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
});
const { advanceTime, messaging, openDiscuss } = await start({
discuss: {
context: { active_id: mailChannelId1 },
},
hasTimeControl: true,
});
await openDiscuss();
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"",
"Should display no one is currently typing"
);
// simulate receive typing notification from demo "is typing"
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': true,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Demo is typing...",
"Should display that demo user is typing"
);
await afterNextRender(() => advanceTime(60 * 1000));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"",
"Should no longer display that demo user is typing"
);
});
QUnit.test ('other member typing status "is typing" refreshes 60 seconds timer of assuming no longer typing', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
});
const { advanceTime, messaging, openDiscuss } = await start({
discuss: {
context: { active_id: mailChannelId1 },
},
hasTimeControl: true,
});
await openDiscuss();
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"",
"Should display no one is currently typing"
);
// simulate receive typing notification from demo "is typing"
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': true,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Demo is typing...",
"Should display that demo user is typing"
);
// simulate receive typing notification from demo "is typing" again after 50s.
await advanceTime(50 * 1000);
messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': true,
},
});
await advanceTime(50 * 1000);
await nextAnimationFrame();
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Demo is typing...",
"Should still display that demo user is typing after 100 seconds (refreshed is typing status at 50s => (100 - 50) = 50s < 60s after assuming no-longer typing)"
);
await afterNextRender(() => advanceTime(11 * 1000));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"",
"Should no longer display that demo user is typing after 111 seconds (refreshed is typing status at 50s => (111 - 50) = 61s > 60s after assuming no-longer typing)"
);
});
QUnit.test('receive several other members typing status "is typing"', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([
{ name: 'Other 10' },
{ name: 'Other 11' },
{ name: 'Other 12' },
]);
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
[0, 0, { partner_id: resPartnerId2 }],
[0, 0, { partner_id: resPartnerId3 }],
],
});
const { messaging, openDiscuss } = await start({
discuss: {
context: { active_id: mailChannelId1 },
},
});
await openDiscuss();
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"",
"Should display no one is currently typing"
);
// simulate receive typing notification from other 10 (is typing)
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': true,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Other 10 is typing...",
"Should display that 'Other 10' member is typing"
);
// simulate receive typing notification from other 11 (is typing)
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId2,
},
'is_typing': true,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Other 10 and Other 11 are typing...",
"Should display that members 'Other 10' and 'Other 11' are typing (order: longer typer named first)"
);
// simulate receive typing notification from other 12 (is typing)
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId3,
},
'is_typing': true,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Other 10, Other 11 and more are typing...",
"Should display that members 'Other 10', 'Other 11' and more (at least 1 extra member) are typing (order: longer typer named first)"
);
// simulate receive typing notification from other 10 (no longer is typing)
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': false,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Other 11 and Other 12 are typing...",
"Should display that members 'Other 11' and 'Other 12' are typing ('Other 10' stopped typing)"
);
// simulate receive typing notification from other 10 (is typing again)
await afterNextRender(() => messaging.rpc({
route: '/mail/channel/notify_typing',
params: {
'channel_id': mailChannelId1,
'context': {
'mockedPartnerId': resPartnerId1,
},
'is_typing': true,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Other 11, Other 12 and more are typing...",
"Should display that members 'Other 11' and 'Other 12' and more (at least 1 extra member) are typing (order by longer typer, 'Other 10' just recently restarted typing)"
);
});
});
});

View file

@ -0,0 +1,64 @@
/** @odoo-module **/
import { manageMessages } from "@mail/js/tools/debug_manager";
import { click, getFixture, legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { createWebClient, doAction, getActionManagerServerData } from "@web/../tests/webclient/helpers";
import { registry } from "@web/core/registry";
QUnit.module("DebugMenu");
QUnit.test("Manage Messages", async function (assert) {
assert.expect(6);
patchWithCleanup(odoo, { debug: "1" });
const serverData = getActionManagerServerData();
// Add fake "mail.message" model and arch
serverData.models["mail.message"] = {
fields: { name: { string: "Name", type: "char" } },
records: [],
};
Object.assign(serverData.views, {
"mail.message,false,list": `<tree/>`,
"mail.message,false,form": `<form/>`,
"mail.message,false,search": `<search/>`,
});
registry.category("debug").category("form").add("manageMessages", manageMessages);
async function mockRPC(route, { method, model, kwargs }) {
if (method === "check_access_rights") {
return true;
}
if (method === "web_search_read" && model === "mail.message") {
const { context, domain } = kwargs;
assert.strictEqual(context.default_res_id, 5);
assert.strictEqual(context.default_res_model, "partner");
assert.deepEqual(domain, ["&", ["res_id", "=", 5], ["model", "=", "partner"]]);
}
}
const target = getFixture();
const wc = await createWebClient({ serverData, mockRPC });
await doAction(wc, 3, { viewType: "form", props: { resId: 5 } });
await legacyExtraNextTick();
await click(target, ".o_debug_manager .dropdown-toggle");
const dropdownItems = target.querySelectorAll(
".o_debug_manager .dropdown-menu .dropdown-item"
);
assert.strictEqual(dropdownItems.length, 1);
assert.strictEqual(
dropdownItems[0].innerText.trim(),
"Manage Messages",
"should have correct menu item text"
);
await click(dropdownItems[0]);
await legacyExtraNextTick();
assert.strictEqual(
target.querySelector(".breadcrumb-item.active").innerText.trim(),
"Manage Messages"
);
});

View file

@ -0,0 +1,253 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { click, getFixture, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
import { registry } from "@web/core/registry";
import { makeLegacyCommandService } from "@web/legacy/utils";
import core from 'web.core';
import session from 'web.session';
import makeTestEnvironment from "web.test_env";
import { dom, nextTick } from 'web.test_utils';
let target;
QUnit.module('mail', {}, function () {
QUnit.module('M2XAvatarUserLegacy', {
beforeEach() {
target = getFixture();
},
});
QUnit.test('many2many_avatar_user widget in form view', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' });
const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 });
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] });
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
res_id: m2xAvatarUserId1,
views: [[false, 'form']],
});
await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar'));
assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened');
assert.strictEqual(
document.querySelector('.o_ChatWindowHeader_name').textContent,
'Partner 1',
'First chat window should be related to partner 1'
);
});
QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1] = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_id" widget="many2one_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario")
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Your Company, Mitchell Admin",
"Public user",
"Mario",
"Luigi",
"Yoshi",
])
await click(target, "#o_command_3")
await legacyExtraNextTick();
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi")
});
QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{ name: "Mario" }, { name: "Luigi" }]);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
patchWithCleanup(session, { user_id: [resUsersId2] });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_id" widget="many2one_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario")
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I")
assert.ok(idx >= 0);
// Assign me (Luigi)
triggerHotkey("alt+shift+i")
await legacyExtraNextTick();
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi")
// Unassign me
triggerHotkey("control+k");
await nextTick();
await click([...target.querySelectorAll(".o_command")][idx])
await legacyExtraNextTick();
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "")
});
QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId2] });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Your Company, Mitchell Admin",
"Public user",
"Luigi"
]);
await click(target, "#o_command_2");
await legacyExtraNextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent);
assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]);
});
QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to me"', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1, resUsersId2, resUsersId3] = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId3] });
patchWithCleanup(session, { user_id: [resUsersId2] });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
triggerHotkey("control+k");
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I");
assert.ok(idx >= 0);
// Assign me (Luigi)
triggerHotkey("alt+shift+i");
await legacyExtraNextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]);
// Unassign me
triggerHotkey("control+k");
await nextTick();
await click([...target.querySelectorAll(".o_command")][idx]);
await legacyExtraNextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
});
QUnit.test('avatar_user widget displays the appropriate user image in form view', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" });
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] });
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
res_id: m2xAvatarUserId1,
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'),
`/web/image/res.users/${resUsersId1}/avatar_128`,
'Should have correct avatar image'
);
});
});

View file

@ -0,0 +1,381 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { click, getFixture, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { dom, nextTick } from 'web.test_utils';
import { popoverService } from "@web/core/popover/popover_service";
import { tooltipService } from "@web/core/tooltip/tooltip_service";
let target;
QUnit.module('mail', {}, function () {
QUnit.module('M2XAvatarUser', {
beforeEach() {
target = getFixture();
},
});
QUnit.test('many2one_avatar_user widget in list view', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' });
const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 });
pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
const views = {
'm2x.avatar.user,false,list': '<tree><field name="user_id" widget="many2one_avatar_user"/></tree>',
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
views: [[false, "list"]],
});
await dom.click(document.querySelector('.o_data_cell .o_m2o_avatar > img'));
assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened');
assert.strictEqual(
document.querySelector('.o_ChatWindowHeader_name').textContent,
'Partner 1',
'Chat window should be related to partner 1'
);
});
QUnit.test('many2many_avatar_user widget in form view', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' });
const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 });
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] });
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
res_id: m2xAvatarUserId1,
views: [[false, 'form']],
});
await dom.click(document.querySelector('.o_field_many2many_avatar_user .badge .o_m2m_avatar'));
assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened');
assert.strictEqual(
document.querySelector('.o_ChatWindowHeader_name').textContent,
'Partner 1',
'First chat window should be related to partner 1'
);
});
QUnit.test('many2many_avatar_user in kanban view', async function (assert) {
assert.expect(5);
patchWithCleanup(browser, {
setTimeout: async (fn) => {
await new Promise((r) => setTimeout(r))
fn();
},
});
const pyEnv = await startServer();
const resUsersIds = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }, { name: "Tapu" }],
);
pyEnv['m2x.avatar.user'].create({ user_ids: resUsersIds });
registry.category("services").add("popover", popoverService);
registry.category("services").add("tooltip", tooltipService);
const views = {
'm2x.avatar.user,false,kanban':
`<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="user_id"/>
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="user_ids" widget="many2many_avatar_user"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
views: [[false, 'kanban']],
});
assert.containsOnce(document.body, '.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty',
"should have o_m2m_avatar_empty span");
assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty').innerText.trim(), "+2",
"should have +2 in o_m2m_avatar_empty");
document.querySelector('.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty').dispatchEvent(new Event('mouseenter'));
await nextTick();
assert.containsOnce(document.body, '.popover',
"should open a popover hover on o_m2m_avatar_empty");
assert.strictEqual(document.querySelector('.popover .o-tooltip > div').innerText.trim(), 'Luigi', 'should have a right text in popover');
assert.strictEqual(document.querySelectorAll('.popover .o-tooltip > div')[1].innerText.trim(), 'Tapu', 'should have a right text in popover');
});
QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1] = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_id" widget="many2one_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "Mario")
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Your Company, Mitchell Admin",
"Public user",
"Mario",
"Luigi",
"Yoshi",
])
await click(target, "#o_command_3")
await nextTick();
assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "Luigi")
});
QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{ name: "Mario" }, { name: "Luigi" }]);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" });
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_id" widget="many2one_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "Mario")
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/Unassign to meALT + SHIFT + I")
assert.ok(idx >= 0);
// Assign me (Luigi)
triggerHotkey("alt+shift+i")
await nextTick();
assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "Luigi")
// Unassign me
triggerHotkey("control+k");
await nextTick();
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "")
});
QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId2] });
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Your Company, Mitchell Admin",
"Public user",
"Luigi"
]);
await click(target, "#o_command_2");
await nextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent);
assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]);
});
QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to me"', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1, resUsersId2, resUsersId3] = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId3] });
patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" });
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
triggerHotkey("control+k");
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/Unassign to meALT + SHIFT + I");
assert.ok(idx >= 0);
// Assign me (Luigi)
triggerHotkey("alt+shift+i");
await nextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]);
// Unassign me
triggerHotkey("control+k");
await nextTick();
await click([...target.querySelectorAll(".o_command")][idx]);
await nextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
});
QUnit.test('avatar_user widget displays the appropriate user image in list view', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" });
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
const views = {
'm2x.avatar.user,false,list': '<tree><field name="user_id" widget="many2one_avatar_user"/></tree>',
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
res_id: m2xAvatarUserId1,
views: [[false, 'list']],
});
assert.strictEqual(
document.querySelector('.o_m2o_avatar > img').getAttribute('data-src'),
`/web/image/res.users/${resUsersId1}/avatar_128`,
'Should have correct avatar image'
);
});
QUnit.test('avatar_user widget displays the appropriate user image in kanban view', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" });
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
const views = {
'm2x.avatar.user,false,kanban':
`<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="user_id" widget="many2one_avatar_user"/>
</div>
</t>
</templates>
</kanban>`,
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
res_id: m2xAvatarUserId1,
views: [[false, 'kanban']],
});
assert.strictEqual(
document.querySelector('.o_m2o_avatar > img').getAttribute('data-src'),
`/web/image/res.users/${resUsersId1}/avatar_128`,
'Should have correct avatar image'
);
});
QUnit.test('avatar_user widget displays the appropriate user image in form view', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" });
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] });
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
res_id: m2xAvatarUserId1,
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelector('.o_field_many2many_avatar_user.o_field_widget .badge img').getAttribute('data-src'),
`/web/image/res.users/${resUsersId1}/avatar_128`,
'Should have correct avatar image'
);
});
});

View file

@ -0,0 +1,123 @@
/** @odoo-module **/
import { clear } from '@mail/model/model_field_command';
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('model_field_commands', {}, function () {
QUnit.module('clear_tests.js');
QUnit.test('clear: should set attribute field undefined if there is no default value', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const task = messaging.models['TestTask'].insert({
id: 1,
title: 'test title 1',
});
task.update({ title: clear() });
assert.strictEqual(
task.title,
undefined,
'clear: should set attribute field undefined if there is no default value'
);
});
QUnit.test('clear: should set attribute field the default value', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const task = messaging.models['TestTask'].insert({
id: 1,
difficulty: 5,
});
task.update({ difficulty: clear() });
assert.strictEqual(
task.difficulty,
1,
'clear: should set attribute field the default value'
);
});
QUnit.test('clear: should set x2one field undefined if no default value is given', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
address: { id: 20 },
});
const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 20 });
contact.update({ address: clear() });
assert.strictEqual(
contact.address,
undefined,
'clear: should set x2one field undefined'
);
assert.strictEqual(
address.contact,
undefined,
'the inverse relation should be cleared as well'
);
});
QUnit.test('clear: should set x2one field the default value', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
favorite: { description: 'pingpong' },
id: 10,
});
contact.update({ favorite: clear() });
assert.strictEqual(
contact.favorite.description,
'football',
'clear: should set x2one field default value'
);
});
QUnit.test('clear: should set x2many field empty array if no default value is given', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: { id: 20 },
});
const task = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 });
contact.update({ tasks: clear() });
assert.ok(
contact.tasks instanceof Array &&
contact.tasks.length === 0,
'clear: should set x2many field empty array'
);
assert.strictEqual(
task.responsible,
undefined,
'the inverse relation should be cleared as well'
);
});
QUnit.test('clear: should set x2many field the default value', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
hobbies: [
{ description: 'basketball' },
{ description: 'running' },
{ description: 'photographing' },
],
});
contact.update({ hobbies: clear() });
const hobbyDescriptions = contact.hobbies.map(h => h.description);
assert.deepEqual(
hobbyDescriptions,
['hiking', 'fishing'],
'clear: should set x2many field the default value',
);
});
});
});

View file

@ -0,0 +1,161 @@
/** @odoo-module **/
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('model_field_commands', {}, function () {
QUnit.module('insert_and_replace_tests.js');
QUnit.test('insertAndReplace: should create and link a new record for an empty x2one field', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({ id: 10 });
contact.update({ address: { id: 10 } });
const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 });
assert.strictEqual(
contact.address,
address,
'insertAndReplace: should create and link a record for an empty x2one field'
);
assert.strictEqual(
address.contact,
contact,
'the inverse relation should be set as well'
);
});
QUnit.test('insertAndReplace: should create and replace a new record for a non-empty x2one field', async function (assert) {
assert.expect(3);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
address: { id: 10 },
});
const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 });
contact.update({ address: { id: 20 } });
const address20 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 20 });
assert.strictEqual(
contact.address,
address20,
'insertAndReplace: should create and replace a new record for a non-empty x2one field'
);
assert.strictEqual(
address20.contact,
contact,
'the inverse relation should be set as well'
);
assert.strictEqual(
address10.contact,
undefined,
'the original relation should be dropped'
);
});
QUnit.test('insertAndReplace: should update the existing record for an x2one field', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
address: {
id: 10,
addressInfo: 'address 10',
},
});
const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 });
contact.update({
address: {
id: 10,
addressInfo: 'address 10 updated',
},
});
assert.strictEqual(
contact.address,
address10,
'insertAndReplace: should not drop an existing record'
);
assert.strictEqual(
address10.addressInfo,
'address 10 updated',
'insertAndReplace: should update the existing record for a x2one field'
);
});
QUnit.test('insertAndReplace: should create and replace the records for an x2many field', async function (assert) {
assert.expect(4);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: { id: 10 },
});
const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 });
contact.update({ tasks: { id: 20 } });
const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 });
assert.strictEqual(
contact.tasks.length,
1,
"should have 1 record"
);
assert.strictEqual(
contact.tasks[0],
task20,
'task should be replaced by the new record'
);
assert.strictEqual(
task20.responsible,
contact,
'the inverse relation should be set'
);
assert.strictEqual(
task10.responsible,
undefined,
'the original relation should be dropped'
);
});
QUnit.test('insertAndReplace: should update and replace the records for an x2many field', async function (assert) {
assert.expect(4);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: [
{ id: 10, title: 'task 10' },
{ id: 20, title: 'task 20' },
],
});
const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 });
const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 });
contact.update({
tasks: {
id: 10,
title: 'task 10 updated',
},
});
assert.strictEqual(
contact.tasks.length,
1,
"should have 1 record"
);
assert.strictEqual(
contact.tasks[0],
task10,
'tasks should be replaced by new record'
);
assert.strictEqual(
task10.title,
'task 10 updated',
'the record should be updated'
);
assert.strictEqual(
task20.responsible,
undefined,
'the record should be replaced'
);
});
});
});

View file

@ -0,0 +1,180 @@
/** @odoo-module **/
import { insert } from '@mail/model/model_field_command';
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('model_field_commands', {}, function () {
QUnit.module('insert_tests.js');
QUnit.test('insert: should create and link a new record for an empty x2one field', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({ id: 10 });
contact.update({ address: insert({ id: 10 }) });
const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 });
assert.strictEqual(
contact.address,
address,
'insert: should create and link a record for an empty x2one field'
);
assert.strictEqual(
address.contact,
contact,
'the inverse relation should be set as well'
);
});
QUnit.test('insert: should create and replace a new record for a non-empty x2one field', async function (assert) {
assert.expect(3);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
address: { id: 10 },
});
const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 });
contact.update({ address: insert({ id: 20 }) });
const address20 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 20 });
assert.strictEqual(
contact.address,
address20,
'insert: should create and replace a new record for a non-empty x2one field'
);
assert.strictEqual(
address20.contact,
contact,
'the inverse relation should be set as well'
);
assert.strictEqual(
address10.contact,
undefined,
'the original relation should be dropped'
);
});
QUnit.test('insert: should update the existing record for an x2one field', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
address: {
id: 10,
addressInfo: 'address 10',
},
});
const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 });
contact.update({
address: insert({
id: 10,
addressInfo: 'address 10 updated',
}),
});
assert.strictEqual(
contact.address,
address10,
'insert: should not drop an existing record'
);
assert.strictEqual(
address10.addressInfo,
'address 10 updated',
'insert: should update the existing record for a x2one field'
);
});
QUnit.test('insert: should create and link a new record for an x2many field', async function (assert) {
assert.expect(3);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({ id: 10 });
contact.update({ tasks: insert({ id: 10 }) });
const task = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 });
assert.strictEqual(
contact.tasks.length,
1,
'should have 1 record'
);
assert.strictEqual(
contact.tasks[0],
task,
"should link the new record"
);
assert.strictEqual(
task.responsible,
contact,
'the inverse relation should be set as well'
);
});
QUnit.test('insert: should create and add a new record for an x2many field', async function (assert) {
assert.expect(4);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: { id: 10 },
});
const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 });
contact.update({ tasks: insert({ id: 20 }) });
const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 });
assert.strictEqual(
contact.tasks.length,
2,
"should have 2 records"
);
assert.strictEqual(
contact.tasks[0],
task10,
"the original record should be kept"
);
assert.strictEqual(
contact.tasks[1],
task20,
'new record should be added'
);
assert.strictEqual(
task20.responsible,
contact,
'the inverse relation should be set as well'
);
});
QUnit.test('insert: should update existing records for an x2many field', async function (assert) {
assert.expect(3);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: {
id: 10,
title: 'task 10',
},
});
const task = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 });
contact.update({
tasks: insert({
id: 10,
title: 'task 10 updated',
}),
});
assert.strictEqual(
contact.tasks.length,
1,
"should have 1 record"
);
assert.strictEqual(
contact.tasks[0],
task,
"the original task should be kept"
);
assert.strictEqual(
task.title,
'task 10 updated',
'should update the existing record'
);
});
});
});

View file

@ -0,0 +1,122 @@
/** @odoo-module **/
import { link } from '@mail/model/model_field_command';
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('model_field_commands', {}, function () {
QUnit.module('field_command_link_tests.js');
QUnit.test('link: should link a record to an empty x2one field', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({ id: 10 });
const address = messaging.models['TestAddress'].insert({ id: 10 });
contact.update({ address });
assert.strictEqual(
contact.address,
address,
'link: should link a record to an empty x2one field'
);
assert.strictEqual(
address.contact,
contact,
'the inverse relation should be set as well'
);
});
QUnit.test('link: should replace a record to a non-empty x2one field', async function (assert) {
assert.expect(3);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
address: { id: 10 },
});
const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 });
const address20 = messaging.models['TestAddress'].insert({ id: 20 });
contact.update({ address: address20 });
assert.strictEqual(
contact.address,
address20,
'link: should replace a record to a non-empty x2one field'
);
assert.strictEqual(
address20.contact,
contact,
'the inverse relation should be set as well'
);
assert.strictEqual(
address10.contact,
undefined,
'the orginal relation should be dropped'
);
});
QUnit.test('link: should link a record to an empty x2many field', async function (assert) {
assert.expect(3);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({ id: 10 });
const task = messaging.models['TestTask'].insert({ id: 10 });
contact.update({ tasks: link(task) });
assert.strictEqual(
contact.tasks.length,
1,
"should have 1 record"
);
assert.strictEqual(
contact.tasks[0],
task,
'the record should be linked'
);
assert.strictEqual(
task.responsible,
contact,
'the inverse relation should be set as well'
);
});
QUnit.test('link: should link and add a record to a non-empty x2many field', async function (assert) {
assert.expect(5);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: { id: 10 },
});
const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 });
const task20 = messaging.models['TestTask'].insert({ id: 20 });
contact.update({ tasks: link(task20) });
assert.strictEqual(
contact.tasks.length,
2,
"should have 2 records"
);
assert.strictEqual(
contact.tasks[0],
task10,
"the original record should be kept"
);
assert.strictEqual(
contact.tasks[1],
task20,
"the new record should be added"
);
assert.ok(
contact.tasks instanceof Array &&
contact.tasks.length === 2 &&
contact.tasks.includes(task10) &&
contact.tasks.includes(task20),
'link: should link and add a record to a non-empty x2many field',
);
assert.strictEqual(
task20.responsible,
contact,
'the inverse relation should be set as well'
);
});
});
});

View file

@ -0,0 +1,161 @@
/** @odoo-module **/
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('model_field_commands', {}, function () {
QUnit.module('replace_tests.js');
QUnit.test('replace: should link a record for an empty x2one field', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({ id: 10 });
const address = messaging.models['TestAddress'].insert({ id: 10 });
contact.update({ address });
assert.strictEqual(
contact.address,
address,
'replace: should link a record for an empty x2one field'
);
assert.strictEqual(
address.contact,
contact,
'the inverse relation should be set as well'
);
});
QUnit.test('replace: should replace a record for a non-empty x2one field', async function (assert) {
assert.expect(3);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
address: { id: 10 },
});
const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 });
const address20 = messaging.models['TestAddress'].insert({ id: 20 });
contact.update({ address: address20 });
assert.strictEqual(
contact.address,
address20,
'replace: should replace a record for a non-empty x2one field'
);
assert.strictEqual(
address20.contact,
contact,
'the inverse relation should be set as well'
);
assert.strictEqual(
address10.contact,
undefined,
'the original relation should be dropped'
);
});
QUnit.test('replace: should link a record for an empty x2many field', async function (assert) {
assert.expect(4);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({ id: 10 });
const task = messaging.models['TestTask'].insert({ id: 10 });
contact.update({ tasks: task });
assert.strictEqual(
contact.tasks.length,
1,
"should have 1 record"
);
assert.strictEqual(
contact.tasks.length,
1,
"should have 1 record"
);
assert.strictEqual(
contact.tasks[0],
task,
"the new record should be linked"
);
assert.strictEqual(
task.responsible,
contact,
'the inverse relation should be dropped'
);
});
QUnit.test('replace: should replace all records for a non-empty field', async function (assert) {
assert.expect(5);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: [
{ id: 10 },
{ id: 20 },
],
});
const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 });
const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 });
const task30 = messaging.models['TestTask'].insert({ id: 30 });
contact.update({ tasks: task30 });
assert.strictEqual(
contact.tasks.length,
1,
"should have 1 record"
);
assert.strictEqual(
contact.tasks[0],
task30,
'should be replaced with the new record'
);
assert.strictEqual(
task30.responsible,
contact,
'the inverse relation should be set as well'
);
assert.strictEqual(
task10.responsible,
undefined,
'the original relation should be dropped'
);
assert.strictEqual(
task20.responsible,
undefined,
'the original relation should be dropped'
);
});
QUnit.test('replace: should order the existing records for x2many field', async function (assert) {
assert.expect(3);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: [
{ id: 10 },
{ id: 20 },
],
});
const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 });
const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 });
contact.update({
tasks: [task20, task10],
});
assert.strictEqual(
contact.tasks.length,
2,
"should have 2 records"
);
assert.strictEqual(
contact.tasks[0],
task20,
'records should be re-ordered'
);
assert.strictEqual(
contact.tasks[1],
task10,
'recprds should be re-ordered'
);
});
});
});

View file

@ -0,0 +1,85 @@
/** @odoo-module **/
import {
decrement,
increment,
set
} from '@mail/model/model_field_command';
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('model_field_commands', {}, function () {
QUnit.module('set_tests.js');
QUnit.test('decrement: should decrease attribute field value', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const task = messaging.models['TestTask'].insert({
id: 10,
difficulty: 5,
});
task.update({ difficulty: decrement(2) });
assert.strictEqual(
task.difficulty,
5 - 2,
'decrement: should decrease attribute field value'
);
});
QUnit.test('increment: should increase attribute field value', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const task = messaging.models['TestTask'].insert({
id: 10,
difficulty: 5,
});
task.update({ difficulty: increment(3) });
assert.strictEqual(
task.difficulty,
5 + 3,
'decrement: should increase attribute field value'
);
});
QUnit.test('set: should set a value for attribute field', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const task = messaging.models['TestTask'].insert({
id: 10,
difficulty: 5,
});
task.update({ difficulty: set(20) });
assert.strictEqual(
task.difficulty,
20,
'set: should set a value for attribute field'
);
});
QUnit.test('multiple attribute commands combination', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const task = messaging.models['TestTask'].insert({
id: 10,
difficulty: 5,
});
task.update({
difficulty: [
set(20),
increment(16),
decrement(8),
],
});
assert.strictEqual(
task.difficulty,
20 + 16 - 8,
'multiple attribute commands combination should work as expected'
);
});
});
});

View file

@ -0,0 +1,57 @@
/** @odoo-module **/
import { unlinkAll } from '@mail/model/model_field_command';
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('model_field_commands', {}, function () {
QUnit.module('unlink_all_tests.js');
QUnit.test('unlinkAll: should set x2one field undefined', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
address: { id: 20 },
});
const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 20 });
contact.update({ address: unlinkAll() });
assert.strictEqual(
contact.address,
undefined,
'clear: should set x2one field undefined'
);
assert.strictEqual(
address.contact,
undefined,
'the inverse relation should be cleared as well'
);
});
QUnit.test('unlinkAll: should set x2many field an empty array', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: {
id: 20,
},
});
const task = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 });
contact.update({ tasks: unlinkAll() });
assert.strictEqual(
contact.tasks.length,
0,
'clear: should set x2many field empty array'
);
assert.strictEqual(
task.responsible,
undefined,
'the inverse relation should be cleared as well'
);
});
});
});

View file

@ -0,0 +1,61 @@
/** @odoo-module **/
import { clear, unlink } from '@mail/model/model_field_command';
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('model_field_commands', {}, function () {
QUnit.module('unlink_tests.js');
QUnit.test('unlink: should unlink the record for x2one field', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
address: { id: 10 },
});
const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 });
contact.update({ address: clear() });
assert.strictEqual(
contact.address,
undefined,
'unlink: should unlink the record for x2one field'
);
assert.strictEqual(
address.contact,
undefined,
'the original relation should be dropped as well'
);
});
QUnit.test('unlink: should unlink the specified record for x2many field', async function (assert) {
assert.expect(2);
const { messaging } = await start();
const contact = messaging.models['TestContact'].insert({
id: 10,
tasks: [
{ id: 10 },
{ id: 20 },
],
});
const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 });
const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 });
contact.update({ tasks: unlink(task10) });
assert.ok(
contact.tasks instanceof Array &&
contact.tasks.length === 1 &&
contact.tasks.includes(task20),
'unlink: should unlink the specified record for x2many field'
);
assert.strictEqual(
task10.responsible,
undefined,
'the orignal relation should be dropped as well'
);
});
});
});

View file

@ -0,0 +1,124 @@
/** @odoo-module **/
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('models', {}, function () {
QUnit.module('attachment_tests.js');
QUnit.test('create (txt)', async function (assert) {
assert.expect(9);
const { messaging } = await start();
assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
const attachment = messaging.models['Attachment'].insert({
filename: "test.txt",
id: 750,
mimetype: 'text/plain',
name: "test.txt",
});
assert.ok(attachment);
assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.strictEqual(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }), attachment);
assert.strictEqual(attachment.filename, "test.txt");
assert.strictEqual(attachment.id, 750);
assert.notOk(attachment.isUploading);
assert.strictEqual(attachment.mimetype, 'text/plain');
assert.strictEqual(attachment.name, "test.txt");
});
QUnit.test('displayName', async function (assert) {
assert.expect(5);
const { messaging } = await start();
assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
const attachment = messaging.models['Attachment'].insert({
filename: "test.txt",
id: 750,
mimetype: 'text/plain',
name: "test.txt",
});
assert.ok(attachment);
assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.strictEqual(attachment.displayName, "test.txt");
});
QUnit.test('extension', async function (assert) {
assert.expect(5);
const { messaging } = await start();
assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
const attachment = messaging.models['Attachment'].insert({
filename: "test.txt",
id: 750,
mimetype: 'text/plain',
name: "test.txt",
});
assert.ok(attachment);
assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.strictEqual(attachment.extension, 'txt');
});
QUnit.test('fileType', async function (assert) {
assert.expect(5);
const { messaging } = await start();
assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
const attachment = messaging.models['Attachment'].insert({
filename: "test.txt",
id: 750,
mimetype: 'text/plain',
name: "test.txt",
});
assert.ok(attachment);
assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({
id: 750,
}));
assert.ok(attachment.isText);
});
QUnit.test('isTextFile', async function (assert) {
assert.expect(5);
const { messaging } = await start();
assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
const attachment = messaging.models['Attachment'].insert({
filename: "test.txt",
id: 750,
mimetype: 'text/plain',
name: "test.txt",
});
assert.ok(attachment);
assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.ok(attachment.isText);
});
QUnit.test('isViewable', async function (assert) {
assert.expect(5);
const { messaging } = await start();
assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
const attachment = messaging.models['Attachment'].insert({
filename: "test.txt",
id: 750,
mimetype: 'text/plain',
name: "test.txt",
});
assert.ok(attachment);
assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.ok(attachment.isViewable);
});
});
});

View file

@ -0,0 +1,49 @@
/** @odoo-module **/
import { start } from '@mail/../tests/helpers/test_utils';
import { patchDate } from '@web/../tests/helpers/utils';
QUnit.module('mail', {}, function () {
QUnit.module('models', {}, function () {
QUnit.module('clock_tests.js');
QUnit.test('Deleting all the watchers of a clock should result in the deletion of the clock itself.', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const watcher = messaging.models['ClockWatcher'].insert({
clock: { frequency: 180 * 1000 },
qunitTestOwner: {},
});
const { clock } = watcher;
watcher.delete();
assert.notOk(
clock.exists(),
"deleting all the watchers of a clock should result in the deletion of the clock itself."
);
});
QUnit.test('[technical] Before ticking for the first time, the clock should indicate the date of creation of the record.', async function (assert) {
assert.expect(1);
const { messaging } = await start();
// The date is patched AFTER startup, so if the date field in Clock was set
// at initialization (which we don't want), it will now look completely
// different from the patched date.
patchDate(2016, 8, 8, 14, 55, 15, 352);
const { clock } = messaging.models['ClockWatcher'].insert({
clock: { frequency: 3600 * 1000 },
qunitTestOwner: {},
});
assert.strictEqual(
clock.date.getFullYear(), // no need to be more precise than the year
2016,
"before ticking for the first time, the clock should indicate the date of creation of the record."
);
});
});
});

View file

@ -0,0 +1,110 @@
/* @odoo-module */
import { start, startServer } from "@mail/../tests/helpers/test_utils";
import { patchUiSize, SIZES } from '@mail/../tests/helpers/patch_ui_size';
import {
click,
contains,
createFile,
dropFiles,
dragenterFiles,
triggerEvents,
} from "@web/../tests/utils";
QUnit.module("mail", {}, function () {
QUnit.module("components", {}, function () {
QUnit.module("file_uploader", {}, function () {
QUnit.module("file_uploader_tests.js");
QUnit.test("no conflicts between file uploaders", async function () {
const pyEnv = await startServer();
const resPartnerId1 = pyEnv["res.partner"].create({});
const channelId = pyEnv["mail.channel"].create({});
const { openView } = await start();
// Uploading file in the first thread: res.partner chatter.
await openView({
res_id: resPartnerId1,
res_model: "res.partner",
views: [[false, "form"]],
});
const file1 = await createFile({
name: "text1.txt",
content: "hello, world",
contentType: "text/plain",
});
await dragenterFiles(".o_Chatter", [file1]);
await dropFiles(".o_Chatter_dropZone", [file1]);
// Uploading file in the second thread: mail.channel in chatWindow.
await click(".o_MessagingMenu_toggler");
await click(`.o_ChannelPreviewView[data-channel-id="${channelId}"]`);
const file2 = await createFile({
name: "text2.txt",
content: "hello, world",
contentType: "text/plain",
});
await dragenterFiles(".o_ChatWindow", [file2]);
await dropFiles(".o_ChatWindow .o_DropZone", [file2]);
await contains(".o_ChatWindow .o_Composer .o_AttachmentCard:not(.o-isUploading)");
await triggerEvents(".o_ChatWindow .o_ComposerTextInput_textarea", [
["keydown", { key: "Enter" }],
]);
await contains(".o_Chatter .o_AttachmentCard");
await contains(".o_ChatWindow .o_Message .o_AttachmentCard");
});
QUnit.test('Chatter main attachment: can change from non-viewable to viewable', async function (assert) {
const pyEnv = await startServer();
const resPartnerId = pyEnv['res.partner'].create({});
const irAttachmentId = pyEnv['ir.attachment'].create({
mimetype: 'text/plain',
res_id: resPartnerId,
res_model: 'res.partner',
});
pyEnv['mail.message'].create({
attachment_ids: [irAttachmentId],
model: 'res.partner',
res_id: resPartnerId,
});
pyEnv['res.partner'].write([resPartnerId], {message_main_attachment_id : irAttachmentId})
const views = {
'res.partner,false,form':
'<form string="Partners">' +
'<sheet>' +
'<field name="name"/>' +
'</sheet>' +
'<div class="o_attachment_preview"/>' +
'<div class="oe_chatter">' +
'<field name="message_ids"/>' +
'</div>' +
'</form>',
};
patchUiSize({ size: SIZES.XXL });
const { openFormView } = await start({
mockRPC(route, args) {
if (_.str.contains(route, '/web/static/lib/pdfjs/web/viewer.html')) {
var canvas = document.createElement('canvas');
return canvas.toDataURL();
}
},
serverData: { views },
});
await openFormView({
res_id: resPartnerId,
res_model: 'res.partner',
});
// Add a PDF file
await click(".o_ChatterTopbar_buttonSendMessage");
const pdfFile = await createFile({ name: "invoice.pdf", contentType: "application/pdf" });
await dragenterFiles(".o_Chatter", [pdfFile]);
await dropFiles(".o_Chatter_dropZone", [pdfFile]);
await contains(".o_attachment_preview_container > iframe", { count: 0 }); // The viewer tries to display the text file not the PDF
// Switch to the PDF file in the viewer
await click(".o_move_next");
await contains(".o_attachment_preview_container > iframe"); // There should be iframe for PDF viewer
});
});
});
});

View file

@ -0,0 +1,168 @@
/** @odoo-module **/
import { insert } from '@mail/model/model_field_command';
import { start } from '@mail/../tests/helpers/test_utils';
import { str_to_datetime } from 'web.time';
QUnit.module('mail', {}, function () {
QUnit.module('models', {}, function () {
QUnit.module('message_tests.js');
QUnit.test('create', async function (assert) {
assert.expect(31);
const { messaging } = await start();
assert.notOk(messaging.models['Partner'].findFromIdentifyingData({ id: 5 }));
assert.notOk(messaging.models['Thread'].findFromIdentifyingData({
id: 100,
model: 'mail.channel',
}));
assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.notOk(messaging.models['Message'].findFromIdentifyingData({ id: 4000 }));
const thread = messaging.models['Thread'].insert({
id: 100,
model: 'mail.channel',
name: "General",
});
const message = messaging.models['Message'].insert({
attachments: {
filename: "test.txt",
id: 750,
mimetype: 'text/plain',
name: "test.txt",
},
author: insert({ id: 5, display_name: "Demo" }),
body: "<p>Test</p>",
date: moment(str_to_datetime("2019-05-05 10:00:00")),
id: 4000,
isNeedaction: true,
isStarred: true,
originThread: thread,
});
assert.ok(messaging.models['Partner'].findFromIdentifyingData({ id: 5 }));
assert.ok(messaging.models['Thread'].findFromIdentifyingData({
id: 100,
model: 'mail.channel',
}));
assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }));
assert.ok(messaging.models['Message'].findFromIdentifyingData({ id: 4000 }));
assert.ok(message);
assert.strictEqual(messaging.models['Message'].findFromIdentifyingData({ id: 4000 }), message);
assert.strictEqual(message.body, "<p>Test</p>");
assert.ok(message.date instanceof moment);
assert.strictEqual(
moment(message.date).utc().format('YYYY-MM-DD hh:mm:ss'),
"2019-05-05 10:00:00"
);
assert.strictEqual(message.id, 4000);
assert.strictEqual(message.originThread, messaging.models['Thread'].findFromIdentifyingData({
id: 100,
model: 'mail.channel',
}));
assert.ok(
message.threads.includes(messaging.models['Thread'].findFromIdentifyingData({
id: 100,
model: 'mail.channel',
}))
);
// from partnerId being in needaction_partner_ids
assert.ok(message.threads.includes(messaging.inbox.thread));
// from partnerId being in starred_partner_ids
assert.ok(message.threads.includes(messaging.starred.thread));
const attachment = messaging.models['Attachment'].findFromIdentifyingData({ id: 750 });
assert.ok(attachment);
assert.strictEqual(attachment.filename, "test.txt");
assert.strictEqual(attachment.id, 750);
assert.notOk(attachment.isUploading);
assert.strictEqual(attachment.mimetype, 'text/plain');
assert.strictEqual(attachment.name, "test.txt");
const channel = messaging.models['Thread'].findFromIdentifyingData({
id: 100,
model: 'mail.channel',
});
assert.ok(channel);
assert.strictEqual(channel.model, 'mail.channel');
assert.strictEqual(channel.id, 100);
assert.strictEqual(channel.name, "General");
const partner = messaging.models['Partner'].findFromIdentifyingData({ id: 5 });
assert.ok(partner);
assert.strictEqual(partner.displayName, "Demo");
assert.strictEqual(partner.id, 5);
});
QUnit.test('message without body should be considered empty', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ id: 11 });
assert.ok(message.isEmpty);
});
QUnit.test('message with body "" should be considered empty', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ body: "", id: 11 });
assert.ok(message.isEmpty);
});
QUnit.test('message with body "<p></p>" should be considered empty', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ body: "<p></p>", id: 11 });
assert.ok(message.isEmpty);
});
QUnit.test('message with body "<p><br></p>" should be considered empty', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ body: "<p><br></p>", id: 11 });
assert.ok(message.isEmpty);
});
QUnit.test('message with body "<p><br/></p>" should be considered empty', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ body: "<p><br/></p>", id: 11 });
assert.ok(message.isEmpty);
});
QUnit.test(String.raw`message with body "<p>\n</p>" should be considered empty`, async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ body: "<p>\n</p>", id: 11 });
assert.ok(message.isEmpty);
});
QUnit.test(String.raw`message with body "<p>\r\n\r\n</p>" should be considered empty`, async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ body: "<p>\r\n\r\n</p>", id: 11 });
assert.ok(message.isEmpty);
});
QUnit.test('message with body "<p> </p> " should be considered empty', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ body: "<p> </p> ", id: 11 });
assert.ok(message.isEmpty);
});
QUnit.test(`message with body "<img src=''>" should not be considered empty`, async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ body: "<img src=''>", id: 11 });
assert.notOk(message.isEmpty);
});
QUnit.test('message with body "test" should not be considered empty', async function (assert) {
assert.expect(1);
const { messaging } = await start();
const message = messaging.models['Message'].insert({ body: "test", id: 11 });
assert.notOk(message.isEmpty);
});
});
});

View file

@ -0,0 +1,39 @@
/** @odoo-module **/
import { start } from '@mail/../tests/helpers/test_utils';
import { browser } from '@web/core/browser/browser';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('mail', {}, function () {
QUnit.module('models', {}, function () {
QUnit.module('messaging_menu_tests.js');
QUnit.test('messaging menu counter should ignore unread messages in channels that are unpinned', async function (assert) {
assert.expect(1);
patchWithCleanup(browser, {
Notification: {
...browser.Notification,
permission: 'denied',
},
});
const { messaging } = await start();
messaging.models['Thread'].insert({
channel: {
id: 31,
serverMessageUnreadCounter: 1,
},
id: 31,
isServerPinned: false,
model: 'mail.channel',
});
assert.strictEqual(
messaging.messagingMenu.counter,
0,
"messaging menu counter should ignore unread messages in channels that are unpinned"
);
});
});
});

View file

@ -0,0 +1,311 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
QUnit.module('mail', {}, function () {
QUnit.module('models', {}, function () {
QUnit.module('messaging_tests.js', {}, function () {
QUnit.test('openChat: display notification for partner without user', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const { messaging } = await start({
services: {
notification: makeFakeNotificationService(message => {
assert.ok(
true,
"should display a toast notification after failing to open chat"
);
assert.strictEqual(
message,
"You can only chat with partners that have a dedicated user.",
"should display the correct information in the notification"
);
}),
},
});
await messaging.openChat({ partnerId: resPartnerId1 });
});
QUnit.test('openChat: display notification for wrong user', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
pyEnv['res.users'].create({});
const { messaging } = await start({
services: {
notification: makeFakeNotificationService(message => {
assert.ok(
true,
"should display a toast notification after failing to open chat"
);
assert.strictEqual(
message,
"You can only chat with existing users.",
"should display the correct information in the notification"
);
}),
},
});
// userId not in the server data
await messaging.openChat({ userId: 4242 });
});
QUnit.test('openChat: open new chat for user', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['res.users'].create({ partner_id: resPartnerId1 });
const { messaging } = await start({ data: this.data });
const partner = messaging.models['Partner'].findFromIdentifyingData({ id: resPartnerId1 });
const existingChat = partner ? partner.dmChatWithCurrentPartner : undefined;
assert.notOk(existingChat, 'a chat should not exist with the target partner initially');
await messaging.openChat({ partnerId: resPartnerId1 });
const chat = messaging.models['Partner'].findFromIdentifyingData({ id: resPartnerId1 }).dmChatWithCurrentPartner;
assert.ok(chat, 'a chat should exist with the target partner');
assert.strictEqual(chat.thread.threadViews.length, 1, 'the chat should be displayed in a `ThreadView`');
});
QUnit.test('openChat: open existing chat for user', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
pyEnv['res.users'].create({ partner_id: resPartnerId1 });
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: "chat",
});
const { messaging } = await start();
const existingChat = messaging.models['Partner'].findFromIdentifyingData({ id: resPartnerId1 }).dmChatWithCurrentPartner;
assert.ok(existingChat, 'a chat should initially exist with the target partner');
assert.strictEqual(existingChat.thread.threadViews.length, 0, 'the chat should not be displayed in a `ThreadView`');
await messaging.openChat({ partnerId: resPartnerId1 });
assert.ok(existingChat, 'a chat should still exist with the target partner');
assert.strictEqual(existingChat.id, mailChannelId1, 'the chat should be the existing chat');
assert.strictEqual(existingChat.thread.threadViews.length, 1, 'the chat should now be displayed in a `ThreadView`');
});
QUnit.test('rpc: create from args', async function (assert) {
assert.expect(1);
const { messaging, pyEnv } = await start();
const partnerId = await messaging.rpc({
method: 'create',
model: 'res.partner',
args: [[{ name: 'foo' }]],
});
const [partner] = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]);
assert.strictEqual('foo', partner.name);
});
QUnit.test('rpc: create from kwargs', async function (assert) {
assert.expect(1);
const { messaging, pyEnv } = await start();
const partnerId = await messaging.rpc({
method: 'create',
model: 'res.partner',
kwargs: {
vals_list: [{ name: 'foo' }],
},
});
const [partner] = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]);
assert.strictEqual('foo', partner.name);
});
QUnit.test('rpc: read from args', async function (assert) {
assert.expect(2);
const { messaging, pyEnv } = await start();
const partnerId = pyEnv['res.partner'].create({ name: 'foo' });
const [partner] = await messaging.rpc({
method: 'read',
model: 'res.partner',
args: [[partnerId], ['id', 'name']],
});
assert.strictEqual(partner.name, 'foo');
assert.strictEqual(Object.keys(partner).length, 2);
});
QUnit.test('rpc: read from kwargs', async function (assert) {
assert.expect(2);
const { messaging, pyEnv } = await start();
const partnerId = pyEnv['res.partner'].create({ name: 'foo' });
const [partner] = await messaging.rpc({
method: 'read',
model: 'res.partner',
args: [[partnerId]],
kwargs: {
fields: ['id', 'name'],
}
});
assert.strictEqual(partner.name, 'foo');
assert.strictEqual(Object.keys(partner).length, 2);
});
QUnit.test('rpc: readGroup from args', async function (assert) {
assert.expect(4);
const { messaging, pyEnv } = await start();
const partnerIds = pyEnv['res.partner'].create([
{ name: 'foo' },
{ name: 'foo' },
{ name: 'bar' },
{ name: 'bar' },
]);
const readGroupDomain = [['id', 'in', partnerIds]];
const readGroupFields = ['name'];
const readGroupGroupBy = ['name'];
const [firstGroup, secondGroup] = await messaging.rpc({
method: 'read_group',
model: 'res.partner',
args: [readGroupDomain, readGroupFields, readGroupGroupBy],
});
assert.strictEqual(firstGroup.name, 'bar');
assert.strictEqual(firstGroup.name_count, 2);
assert.strictEqual(secondGroup.name, 'foo');
assert.strictEqual(secondGroup.name_count, 2);
});
QUnit.test('rpc: readGroup from kwargs', async function (assert) {
assert.expect(4);
const { messaging, pyEnv } = await start();
const partnerIds = pyEnv['res.partner'].create([
{ name: 'foo' },
{ name: 'foo' },
{ name: 'bar' },
{ name: 'bar' },
]);
const readGroupDomain = [['id', 'in', partnerIds]];
const readGroupFields = ['name'];
const readGroupGroupBy = ['name'];
const [firstGroup, secondGroup] = await messaging.rpc({
method: 'read_group',
model: 'res.partner',
kwargs: {
domain: readGroupDomain,
fields: readGroupFields,
groupBy: readGroupGroupBy,
},
});
assert.strictEqual(firstGroup.name, 'bar');
assert.strictEqual(firstGroup.name_count, 2);
assert.strictEqual(secondGroup.name, 'foo');
assert.strictEqual(secondGroup.name_count, 2);
});
QUnit.test('rpc: search from args', async function (assert) {
assert.expect(1);
const { messaging, pyEnv } = await start();
const partnerIds = pyEnv['res.partner'].create([{ name: 'foo' }, { name: 'bar' }]);
const searchDomain = [['id', 'in', partnerIds]];
const serverPartnerIds = await messaging.rpc({
model: 'res.partner',
method: 'search',
args: [searchDomain],
});
assert.deepEqual(partnerIds, serverPartnerIds);
});
QUnit.test('rpc: search from kwargs', async function (assert) {
assert.expect(1);
const { messaging, pyEnv } = await start();
const partnerIds = pyEnv['res.partner'].create([{ name: 'foo' }, { name: 'bar' }]);
const searchDomain = [['id', 'in', partnerIds]];
const serverPartnerIds = await messaging.rpc({
model: 'res.partner',
method: 'search',
kwargs: {
domain: searchDomain,
},
});
assert.deepEqual(partnerIds, serverPartnerIds);
});
QUnit.test('rpc: searchRead from args', async function (assert) {
assert.expect(2);
const { messaging, pyEnv } = await start();
const partnerIds = pyEnv['res.partner'].create([{ name: 'foo' }, { name: 'bar' }]);
const searchReadDomain = [['id', 'in', partnerIds]];
const searchReadFields = ['id', 'name'];
const [firstPartner, secondPartner] = await messaging.rpc({
model: 'res.partner',
method: 'search_read',
args: [searchReadDomain, searchReadFields],
});
assert.strictEqual(firstPartner.name, 'foo');
assert.strictEqual(secondPartner.name, 'bar');
});
QUnit.test('rpc: write from args', async function (assert) {
assert.expect(1);
const { messaging, pyEnv } = await start();
const partnerId = pyEnv['res.partner'].create({ name: 'foo' });
await messaging.rpc({
method: 'write',
model: 'res.partner',
args: [[partnerId], { name: 'bar' }],
});
const [partner] = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]);
assert.strictEqual(partner.name, 'bar');
});
QUnit.test('rpc: write from kwargs', async function (assert) {
assert.expect(1);
const { messaging, pyEnv } = await start();
const partnerId = pyEnv['res.partner'].create({ name: 'foo' });
await messaging.rpc({
method: 'write',
model: 'res.partner',
args: [[partnerId]],
kwargs: {
vals: { name: 'bar' },
},
});
const [partner] = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]);
assert.strictEqual(partner.name, 'bar');
});
QUnit.test('rpc: unlink', async function (assert) {
assert.expect(1);
const { messaging, pyEnv } = await start();
const partnerId = pyEnv['res.partner'].create({ name: 'foo' });
await messaging.rpc({
method: 'unlink',
model: 'res.partner',
args: [[partnerId]],
});
const searchReadResults = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]);
assert.strictEqual(searchReadResults.length, 0);
});
});
});
});

View file

@ -0,0 +1,100 @@
/** @odoo-module **/
import { editInput, getFixture, mockTimeout, nextTick, triggerEvent } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { dom } from 'web.test_utils';
let target;
let serverData;
QUnit.module('mail', {}, function () {
QUnit.module('onchange on keydown', {
async beforeEach() {
target = getFixture();
serverData = {
models: {
'res.partner': {
fields: {
id: {type: 'integer'},
description: {type: 'text'},
display_name: {type: 'char'},
},
records: [{
id: 1,
description: '',
display_name: 'first record',
}],
onchanges: {
description: () => {},
},
}
}
};
setupViewRegistries();
}
}, function () {
QUnit.test('Test that onchange_on_keydown option triggers the onchange properly', async function (assert) {
assert.expect(3);
await makeView({
type: "form",
resModel: 'res.partner',
serverData,
arch: '<form><field name="description" onchange_on_keydown="True" keydown_debounce_delay="0"/></form>',
mockRPC(route, params) {
if (params.method === 'onchange') {
// the onchange will be called twice: at record creation & when keydown is detected
// the second call should have our description value completed.
assert.ok(true);
if (params.args[1] && params.args[1].description === 'testing the keydown event') {
assert.ok(true);
}
return {};
}
}
});
const textarea = target.querySelector('textarea[id="description"]');
await dom.click(textarea);
for (let key of 'testing the keydown event') {
// trigger each key separately to simulate a user typing
textarea.value = textarea.value + key;
await dom.triggerEvent(textarea, 'input', { key: key });
};
// only trigger the keydown when typing ends to avoid getting a lot of onchange since the
// delay is set to 0 for test purposes
// for real use cases there will be a debounce delay set to avoid spamming the event
await dom.triggerEvent(textarea, 'keydown');
await nextTick();
});
QUnit.test(
"Editing a text field with the onchange_on_keydown option disappearing shouldn't trigger a crash",
async function (assert) {
const { execRegisteredTimeouts } = mockTimeout();
await makeView({
type: "form",
resModel: 'res.partner',
serverData,
resId: 1,
arch: `
<form>
<field name="description" onchange_on_keydown="True" attrs="{'invisible': [('display_name','=','yop')]}"/>
<field name="display_name"/>
</form>`,
mockRPC(route, params) {
if (params.method === 'onchange') {
assert.step('onchange');
}
}
});
await triggerEvent(target, 'textarea[id="description"]', { key: "blabla" });
await triggerEvent(target, 'textarea[id="description"]', 'keydown');
await editInput(target, "[name=display_name] input", "yop");
await execRegisteredTimeouts();
assert.verifySteps([]);
});
});
});

View file

@ -0,0 +1,230 @@
/** @odoo-module **/
import * as utils from '@mail/js/utils';
import { start, startServer } from "@mail/../tests/helpers/test_utils";
QUnit.module('mail', {}, function () {
QUnit.module('Mail utils');
QUnit.test('add_link utility function', function (assert) {
assert.expect(29);
var testInputs = {
'http://admin:password@example.com:8/%2020': true,
'https://admin:password@example.com/test': true,
'www.example.com:8/test': true,
'https://127.0.0.5:8069': true,
'www.127.0.0.5': false,
'should.notmatch': false,
'fhttps://test.example.com/test': false,
"https://www.transifex.com/odoo/odoo-11/translate/#fr/lunch?q=text%3A'La+Tartiflette'": true,
'https://www.transifex.com/odoo/odoo-11/translate/#fr/$/119303430?q=text%3ATartiflette': true,
'https://tenor.com/view/chỗgiặt-dog-smile-gif-13860250': true,
'http://www.boîtenoire.be': true,
// Subdomain different than `www` with long domain name
'https://xyz.veryveryveryveryverylongdomainname.com/example': true,
// Two subdomains
'https://abc.xyz.veryveryveryveryverylongdomainname.com/example': true,
// Long domain name with www
'https://www.veryveryveryveryverylongdomainname.com/example': true,
// Subdomain with numbers
'https://www.45017478-master-all.runbot134.odoo.com/web': true,
"https://x.com": true,
};
_.each(testInputs, function (willLinkify, content) {
var output = utils.parseAndTransform(content, utils.addLink);
if (willLinkify) {
assert.strictEqual(output.indexOf('<a '), 0, "There should be a link");
assert.strictEqual(output.indexOf('</a>'), (output.length - 4), "Link should match the whole text");
} else {
assert.strictEqual(output.indexOf('<a '), -1, "There should be no link");
}
});
});
QUnit.test('addLink: utility function and special entities', function (assert) {
assert.expect(8);
var testInputs = {
// textContent not unescaped
'<p>https://example.com/?&amp;currency_id</p>':
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/?&amp;currency_id">https://example.com/?&amp;currency_id</a></p>',
// entities not unescaped
'&amp; &amp;amp; &gt; &lt;': '&amp; &amp;amp; &gt; &lt;',
// > and " not linkified since they are not in URL regex
'<p>https://example.com/&gt;</p>':
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/">https://example.com/</a>&gt;</p>',
'<p>https://example.com/"hello"&gt;</p>':
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/">https://example.com/</a>"hello"&gt;</p>',
// & and ' linkified since they are in URL regex
'<p>https://example.com/&amp;hello</p>':
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/&amp;hello">https://example.com/&amp;hello</a></p>',
'<p>https://example.com/\'yeah\'</p>':
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/\'yeah\'">https://example.com/\'yeah\'</a></p>',
// normal character should not be escaped
':\'(': ':\'(',
// special character in smileys should be escaped
'&lt;3': '&lt;3',
};
_.each(testInputs, function (result, content) {
var output = utils.parseAndTransform(content, utils.addLink);
assert.strictEqual(output, result);
});
});
QUnit.test('addLink: linkify inside text node (1 occurrence)', function (assert) {
assert.expect(5);
const content = '<p>some text https://somelink.com</p>';
const linkified = utils.parseAndTransform(content, utils.addLink);
assert.ok(
linkified.startsWith('<p>some text <a'),
"linkified text should start with non-linkified start part, followed by an '<a>' tag"
);
assert.ok(
linkified.endsWith('</a></p>'),
"linkified text should end with closing '<a>' tag"
);
// linkify may add some attributes. Since we do not care of their exact
// stringified representation, we continue deeper assertion with query
// selectors.
const fragment = document.createDocumentFragment();
const div = document.createElement('div');
fragment.appendChild(div);
div.innerHTML = linkified;
assert.strictEqual(
div.textContent,
'some text https://somelink.com',
"linkified text should have same text content as non-linkified version"
);
assert.strictEqual(
div.querySelectorAll(':scope a').length,
1,
"linkified text should have an <a> tag"
);
assert.strictEqual(
div.querySelector(':scope a').textContent,
'https://somelink.com',
"text content of link should be equivalent of its non-linkified version"
);
});
QUnit.test('addLink: linkify inside text node (2 occurrences)', function (assert) {
assert.expect(4);
// linkify may add some attributes. Since we do not care of their exact
// stringified representation, we continue deeper assertion with query
// selectors.
const content = '<p>some text https://somelink.com and again https://somelink2.com ...</p>';
const linkified = utils.parseAndTransform(content, utils.addLink);
const fragment = document.createDocumentFragment();
const div = document.createElement('div');
fragment.appendChild(div);
div.innerHTML = linkified;
assert.strictEqual(
div.textContent,
'some text https://somelink.com and again https://somelink2.com ...',
"linkified text should have same text content as non-linkified version"
);
assert.strictEqual(
div.querySelectorAll(':scope a').length,
2,
"linkified text should have 2 <a> tags"
);
assert.strictEqual(
div.querySelectorAll(':scope a')[0].textContent,
'https://somelink.com',
"text content of 1st link should be equivalent to its non-linkified version"
);
assert.strictEqual(
div.querySelectorAll(':scope a')[1].textContent,
'https://somelink2.com',
"text content of 2nd link should be equivalent to its non-linkified version"
);
});
QUnit.test("url", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["mail.channel"].create({ name: "General" });
const { click, insertText, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
// see: https://www.ietf.org/rfc/rfc1738.txt
const messageBody = "https://odoo.com?test=~^|`{}[]#";
await insertText(".o_ComposerTextInput_textarea", messageBody);
await click("button:contains(Send)");
assert.containsOnce($, `.o_Message a:contains(${messageBody})`);
});
QUnit.test("url with comma at the end", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["mail.channel"].create({ name: "General" });
const { click, insertText, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
const messageBody = "Go to https://odoo.com, it's great!";
await insertText(".o_ComposerTextInput_textarea", messageBody);
await click("button:contains(Send)");
assert.containsOnce($, `.o_Message a:contains(https://odoo.com)`);
assert.containsOnce($, `.o_Message:contains(${messageBody})`);
});
QUnit.test("url with dot at the end", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["mail.channel"].create({ name: "General" });
const { click, insertText, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
const messageBody = "Go to https://odoo.com. It's great!";
await insertText(".o_ComposerTextInput_textarea", messageBody);
await click("button:contains(Send)");
assert.containsOnce($, `.o_Message a:contains(https://odoo.com)`);
assert.containsOnce($, `.o_Message:contains(${messageBody})`);
});
QUnit.test("url with semicolon at the end", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["mail.channel"].create({ name: "General" });
const { click, insertText, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
const messageBody = "Go to https://odoo.com; it's great!";
await insertText(".o_ComposerTextInput_textarea", messageBody);
await click("button:contains(Send)");
assert.containsOnce($, `.o_Message a:contains(https://odoo.com)`);
assert.containsOnce($, `.o_Message:contains(${messageBody})`);
});
QUnit.test("url with ellipsis at the end", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["mail.channel"].create({ name: "General" });
const { click, insertText, openDiscuss } = await start({
discuss: {
context: { active_id: channelId },
},
});
await openDiscuss();
const messageBody = "Go to https://odoo.com... it's great!";
await insertText(".o_ComposerTextInput_textarea", messageBody);
await click("button:contains(Send)");
assert.containsOnce($, `.o_Message a:contains(https://odoo.com)`);
assert.containsOnce($, `.o_Message:contains(${messageBody})`);
});
});

View file

@ -0,0 +1,174 @@
/** @odoo-module **/
import { start } from '@mail/../tests/helpers/test_utils';
import { nextTick } from '@mail/utils/utils';
QUnit.module('mail', {}, function () {
QUnit.module('utils', {}, function () {
QUnit.module('throttle', {}, function () {
QUnit.module('throttle_tests.js', {});
QUnit.test('single call', async function (assert) {
assert.expect(3);
const { advanceTime, messaging } = await start({
hasTimeControl: true,
});
let hasInvokedFunc = false;
const throttle = messaging.models['Throttle'].insert({
func: () => hasInvokedFunc = true,
qunitTestOwner1: {},
});
assert.notOk(
hasInvokedFunc,
"func should not have been invoked on immediate throttle initialization"
);
await advanceTime(0);
assert.notOk(
hasInvokedFunc,
"func should not have been invoked from throttle initialization after 0ms"
);
throttle.do();
await nextTick();
assert.ok(
hasInvokedFunc,
"func should have been immediately invoked on first throttle call"
);
});
QUnit.test('2nd (throttled) call', async function (assert) {
assert.expect(4);
const { advanceTime, messaging } = await start({
hasTimeControl: true,
});
let funcCalledAmount = 0;
const throttle = messaging.models['Throttle'].insert({
func: () => funcCalledAmount++,
qunitTestOwner2: {},
});
throttle.do();
await nextTick();
assert.strictEqual(
funcCalledAmount,
1,
"throttle call return should forward result of inner func 1"
);
throttle.do();
await nextTick();
assert.strictEqual(
funcCalledAmount,
1,
"inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)"
);
await advanceTime(999);
assert.strictEqual(
funcCalledAmount,
1,
"inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)"
);
await advanceTime(1);
assert.strictEqual(
funcCalledAmount,
2,
"throttle call return should forward result of inner func 2"
);
});
QUnit.test('throttled call reinvocation', async function (assert) {
assert.expect(4);
const { advanceTime, messaging } = await start({
hasTimeControl: true,
});
let funcCalledAmount = 0;
const throttle = messaging.models['Throttle'].insert({
func: () => funcCalledAmount++,
qunitTestOwner2: {},
});
throttle.do();
await nextTick();
assert.strictEqual(
funcCalledAmount,
1,
"throttle call return should forward result of inner func 1"
);
throttle.do();
await nextTick();
assert.strictEqual(
funcCalledAmount,
1,
"inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)"
);
await advanceTime(999);
assert.strictEqual(
funcCalledAmount,
1,
"inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)"
);
throttle.do();
await nextTick();
await advanceTime(1);
assert.strictEqual(
funcCalledAmount,
2,
"throttle call return should forward result of inner func 2"
);
});
QUnit.test('clear throttled call', async function (assert) {
assert.expect(4);
const { advanceTime, messaging } = await start({
hasTimeControl: true,
});
let funcCalledAmount = 0;
const throttle = messaging.models['Throttle'].insert({
func: () => funcCalledAmount++,
qunitTestOwner2: {},
});
throttle.do();
await nextTick();
assert.strictEqual(
funcCalledAmount,
1,
"inner function of throttle should have been invoked on 1st call (immediate return)"
);
throttle.do();
await nextTick();
assert.strictEqual(
funcCalledAmount,
1,
"inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)"
);
await advanceTime(500);
assert.strictEqual(
funcCalledAmount,
1,
"inner function of throttle should not have been invoked after 500ms of 2nd call (throttled with 1s internal clock)"
);
throttle.clear();
await nextTick();
throttle.do();
await nextTick();
assert.strictEqual(
funcCalledAmount,
2,
"3rd throttle function call should have invoke inner function immediately (`.clear()` flushes throttle)"
);
});
});
});
});

View file

@ -0,0 +1,68 @@
/** @odoo-module **/
import { start } from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('utils', {}, function () {
QUnit.module('timer', {}, function () {
QUnit.module('timer_tests.js', {});
QUnit.test('timer insert (duration: 0ms)', async function (assert) {
assert.expect(2);
const { advanceTime, messaging } = await start({ hasTimeControl: true });
const timer = messaging.models['Timer'].insert({
qunitTestOwner1: {},
});
assert.ok(
timer.timeoutId,
"timer should not have timed out immediately after insert"
);
await advanceTime(0);
assert.notOk(
timer.timeoutId,
"timer should have timed out on insert after 0ms"
);
});
QUnit.test('timer insert (duration: 1000s)', async function (assert) {
assert.expect(5);
const { advanceTime, messaging } = await start({ hasTimeControl: true });
const timer = messaging.models['Timer'].insert({
qunitTestOwner2: {},
});
assert.ok(
timer.timeoutId,
"timer should not have timed out immediately after insert"
);
await advanceTime(0);
assert.ok(
timer.timeoutId,
"timer should not have timed out on insert after 0ms"
);
await advanceTime(1000);
assert.ok(
timer.timeoutId,
"timer should not have timed out on insert after 1000ms"
);
await advanceTime(998 * 1000 + 999);
assert.ok(
timer.timeoutId,
"timer should not have timed out on insert after 9999ms"
);
await advanceTime(1);
assert.notOk(
timer.timeoutId,
"timer should have timed out on insert after 10s"
);
});
});
});
});

View file

@ -0,0 +1,49 @@
/* @odoo-module */
import { startServer } from "@bus/../tests/helpers/mock_python_environment";
import { start } from "@mail/../tests/helpers/test_utils";
import { commandService } from "@web/core/commands/command_service";
import { registry } from "@web/core/registry";
import { triggerHotkey } from "@web/../tests/helpers/utils";
import { click, contains, insertText } from "@web/../tests/utils";
const serviceRegistry = registry.category("services");
const commandSetupRegistry = registry.category("command_setup");
QUnit.module("mail", {}, function () {
QUnit.module("webclient", function () {
QUnit.module("commands", function () {
QUnit.module("mail_providers_tests.js", {
beforeEach() {
serviceRegistry.add("command", commandService);
registry.category("command_categories").add("default", { label: "default" });
},
});
QUnit.test("open the chatWindow of a user from the command palette", async () => {
const { advanceTime } = await start({ hasTimeControl: true });
triggerHotkey("control+k");
await insertText(".o_command_palette_search input", "@");
advanceTime(commandSetupRegistry.get("@").debounceDelay);
await contains(".o_command", { count: 1 });
await click(".o_command.focused", { text: "Mitchell Admin" });
await contains(".o_ChatWindow", { text: "Mitchell Admin" });
});
QUnit.test("open the chatWindow of a channel from the command palette", async () => {
const pyEnv = await startServer();
pyEnv["mail.channel"].create({ name: "general" });
pyEnv["mail.channel"].create({ name: "project" });
const { advanceTime } = await start({ hasTimeControl: true });
triggerHotkey("control+k");
await insertText(".o_command_palette_search input", "#");
advanceTime(commandSetupRegistry.get("#").debounceDelay);
await contains(".o_command", { count: 2 });
await click(".o_command.focused", { text: "general" });
await contains(".o_ChatWindow", { text: "general" });
});
});
});
});

View file

@ -0,0 +1,69 @@
/** @odoo-module **/
import { click, editInput } from '@web/../tests/helpers/utils';
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
QUnit.module('mail', {}, () => {
QUnit.module('widgets', {}, (hooks) => {
hooks.beforeEach(() => {
serverData = {
models: {
partner: {
fields: {
qux: { string: "Qux", type: "char", trim: true }
}
}
}
}
setupViewRegistries();
});
QUnit.module("emojis_char_field_tests.js");
QUnit.test("emojis_char_field_tests widget: insert emoji at end of word", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="qux" widget="char_emojis"/>
</form>
`,
});
const inputName = document.querySelector('input#qux')
await editInput(inputName, null, "Hello");
assert.strictEqual(inputName.value, "Hello");
click(document, '.o_mail_add_emoji button');
click(document, '.o_mail_emoji[data-emoji=":)"]');
assert.strictEqual(inputName.value, "Hello😊");
});
QUnit.test("emojis_char_field_tests widget: insert emoji as new word", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="qux" widget="char_emojis"/>
</form>
`,
});
const inputName = document.querySelector('input#qux')
await editInput(inputName, null, "Hello ");
assert.strictEqual(inputName.value, "Hello ");
click(document, '.o_mail_add_emoji button');
click(document, '.o_mail_emoji[data-emoji=":)"]');
assert.strictEqual(inputName.value, "Hello 😊");
});
});
});

View file

@ -0,0 +1,92 @@
/** @odoo-module **/
import { start } from '@mail/../tests/helpers/test_utils';
import { browser } from '@web/core/browser/browser';
import { patchWithCleanup } from "@web/../tests/helpers/utils";
QUnit.module('mail', {}, function () {
QUnit.module('widgets', {}, function () {
QUnit.module('notification_alert_tests.js');
QUnit.test('notification_alert widget: display blocked notification alert', async function (assert) {
assert.expect(1);
const views = {
'mail.message,false,form': `<form><widget name="notification_alert"/></form>`,
};
patchWithCleanup(browser, {
Notification: {
...browser.Notification,
permission: 'denied',
},
});
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'mail.message',
views: [[false, 'form']],
});
assert.containsOnce(
document.body,
'.o_NotificationAlert',
"Blocked notification alert should be displayed"
);
});
QUnit.test('notification_alert widget: no notification alert when granted', async function (assert) {
assert.expect(1);
const views = {
'mail.message,false,form': `<form><widget name="notification_alert"/></form>`,
};
patchWithCleanup(browser, {
Notification: {
permission: 'granted',
},
});
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'mail.message',
views: [[false, 'form']],
});
assert.containsNone(
document.body,
'.o_NotificationAlert',
"Blocked notification alert should not be displayed"
);
});
QUnit.test('notification_alert widget: no notification alert when default', async function (assert) {
assert.expect(1);
const views = {
'mail.message,false,form': `<form><widget name="notification_alert"/></form>`,
};
patchWithCleanup(browser, {
Notification: {
permission: 'default',
},
});
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'mail.message',
views: [[false, 'form']],
});
assert.containsNone(
document.body,
'.o_NotificationAlert',
"Blocked notification alert should not be displayed"
);
});
});
});

View file

@ -0,0 +1,30 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
tour.register('mail/static/tests/tours/discuss_public_tour.js', {
test: true,
}, [{
trigger: '.o_DiscussPublicView',
extraTrigger: '.o_ThreadView',
}, {
content: "Check that we are on channel page",
trigger: '.o_ThreadView',
run() {
if (!window.location.pathname.startsWith('/discuss/channel')) {
console.error('Did not automatically redirect to channel page');
}
// Wait for modules to be loaded or failed for the next step
odoo.__DEBUG__.didLogInfo.then(() => {
const { missing, failed, unloaded } = odoo.__DEBUG__.jsModules;
if ([missing, failed, unloaded].some(arr => arr.length)) {
console.error("Couldn't load all JS modules.", JSON.stringify({ missing, failed, unloaded }));
}
document.body.classList.add('o_mail_channel_public_modules_loaded');
});
},
extraTrigger: '.o_mail_channel_public_modules_loaded',
}, {
content: "Wait for all modules loaded check in previous step",
trigger: '.o_mail_channel_public_modules_loaded',
}]);

View file

@ -0,0 +1,31 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
tour.register('mail/static/tests/tours/mail_channel_as_guest_tour.js', {
test: true,
}, [{
content: "Click join",
trigger: '.o_WelcomeView_joinButton',
extraTrigger: '.o_ThreadView',
}, {
content: "Check that we are on channel page",
trigger: '.o_ThreadView',
run() {
if (!window.location.pathname.startsWith('/discuss/channel')) {
console.error('Clicking on join button did not redirect to channel page');
}
// Wait for modules to be loaded or failed for the next step
odoo.__DEBUG__.didLogInfo.then(() => {
const { missing, failed, unloaded } = odoo.__DEBUG__.jsModules;
if ([missing, failed, unloaded].some(arr => arr.length)) {
console.error("Couldn't load all JS modules.", JSON.stringify({ missing, failed, unloaded }));
}
document.body.classList.add('o_mail_channel_as_guest_tour_modules_loaded');
});
},
extraTrigger: '.o_mail_channel_as_guest_tour_modules_loaded',
}, {
content: "Wait for all modules loaded check in previous step",
trigger: '.o_mail_channel_as_guest_tour_modules_loaded',
}]);

View file

@ -0,0 +1,97 @@
/** @odoo-module **/
import {
createFile,
inputFiles,
} from 'web.test_utils_file';
import { contains } from '@web/../tests/utils';
import tour from 'web_tour.tour';
/**
* This tour depends on data created by python test in charge of launching it.
* It is not intended to work when launched from interface. It is needed to test
* an action (action manager) which is not possible to test with QUnit.
* @see mail/tests/test_mail_full_composer.py
*/
tour.register('mail/static/tests/tours/mail_full_composer_test_tour.js', {
test: true,
}, [{
content: "Wait for the chatter to be fully loaded",
trigger: ".o_Chatter",
async run() {
await contains(".o_Message", { count: 1 });
document.body.setAttribute("data-found-message", 1);
},
}, {
content: "Click on Send Message",
trigger: '.o_ChatterTopbar_buttonSendMessage',
extra_trigger: 'body[data-found-message=1]',
}, {
content: "Write something in composer",
trigger: '.o_ComposerTextInput_textarea',
run: 'text blahblah',
}, {
content: "Add one file in composer",
trigger: '.o_Composer_buttonAttachment',
async run() {
const file = await createFile({
content: 'hello, world',
contentType: 'text/plain',
name: 'text.txt',
});
const messaging = await odoo.__DEBUG__.messaging;
const uploader = messaging.models['ComposerView'].all()[0].fileUploader;
inputFiles(
uploader.fileInput,
[file]
);
},
}, {
content: "Open full composer",
trigger: '.o_Composer_buttonFullComposer',
extra_trigger: '.o_AttachmentCard:not(.o-isUploading)' // waiting the attachment to be uploaded
}, {
content: "Check the earlier provided attachment is listed",
trigger: '.o_AttachmentCard[title="text.txt"]',
run() {},
}, {
content: "Check subject is autofilled",
trigger: '[name="subject"] input',
run() {
const subjectValue = document.querySelector('[name="subject"] input').value;
if (subjectValue !== "Re: Jane") {
console.error(
`Full composer should have "Re: Jane" in subject input (actual: ${subjectValue})`
);
}
},
}, {
content: "Check composer content is kept",
trigger: '.o_field_html[name="body"]',
run() {
const bodyContent = document.querySelector('.o_field_html[name="body"]').textContent;
if (!bodyContent.includes("blahblah")) {
console.error(
`Full composer should contain text from small composer ("blahblah") in body input (actual: ${bodyContent})`
);
}
},
}, {
content: "Open templates",
trigger: '.o_field_widget[name="template_id"] input',
}, {
content: "Check a template is listed",
in_modal: false,
trigger: '.ui-autocomplete .ui-menu-item a:contains("Test template")',
run() {},
}, {
content: "Send message",
trigger: '.o_mail_send',
}, {
content: "Check message is shown",
trigger: '.o_Message:contains("blahblah")',
}, {
content: "Check message contains the attachment",
trigger: '.o_Message .o_AttachmentCard_filename:contains("text.txt")',
}]);

View file

@ -0,0 +1,26 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
/**
* Verify that a user can modify their own profile information.
*/
tour.register('mail/static/tests/tours/user_modify_own_profile_tour.js', {
test: true,
}, [{
content: 'Open user account menu',
trigger: '.o_user_menu button',
}, {
content: "Open preferences / profile screen",
trigger: '[data-menu=settings]',
}, {
content: "Update the email address",
trigger: 'div[name="email"] input',
run: 'text updatedemail@example.com',
}, {
content: "Save the form",
trigger: 'button[name="preference_save"]',
}, {
content: "Wait until the modal is closed",
trigger: 'body:not(.modal-open)',
}]);