mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-19 06:32:02 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
2586
odoo-bringout-oca-ocb-mail/mail/static/tests/helpers/mock_server.js
Normal file
2586
odoo-bringout-oca-ocb-mail/mail/static/tests/helpers/mock_server.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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 },
|
||||
]);
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
@ -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'
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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']");
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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}`]);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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><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:<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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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'"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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)"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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)"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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."
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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/?&currency_id</p>':
|
||||
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/?&currency_id">https://example.com/?&currency_id</a></p>',
|
||||
// entities not unescaped
|
||||
'& &amp; > <': '& &amp; > <',
|
||||
// > and " not linkified since they are not in URL regex
|
||||
'<p>https://example.com/></p>':
|
||||
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/">https://example.com/</a>></p>',
|
||||
'<p>https://example.com/"hello"></p>':
|
||||
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/">https://example.com/</a>"hello"></p>',
|
||||
// & and ' linkified since they are in URL regex
|
||||
'<p>https://example.com/&hello</p>':
|
||||
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/&hello">https://example.com/&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
|
||||
'<3': '<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})`);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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)"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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" });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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 😊");
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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',
|
||||
}]);
|
||||
|
|
@ -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',
|
||||
}]);
|
||||
|
|
@ -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")',
|
||||
}]);
|
||||
|
|
@ -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)',
|
||||
}]);
|
||||
Loading…
Add table
Add a link
Reference in a new issue