summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components/discuss
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/components/discuss
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/discuss')
-rw-r--r--addons/mail/static/src/components/discuss/discuss.js313
-rw-r--r--addons/mail/static/src/components/discuss/discuss.scss114
-rw-r--r--addons/mail/static/src/components/discuss/discuss.xml106
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js408
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js725
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js1180
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js238
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_sidebar_tests.js163
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_tests.js4447
9 files changed, 7694 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/discuss/discuss.js b/addons/mail/static/src/components/discuss/discuss.js
new file mode 100644
index 00000000..068fd88d
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/discuss.js
@@ -0,0 +1,313 @@
+odoo.define('mail/static/src/components/discuss/discuss.js', function (require) {
+'use strict';
+
+const components = {
+ AutocompleteInput: require('mail/static/src/components/autocomplete_input/autocomplete_input.js'),
+ Composer: require('mail/static/src/components/composer/composer.js'),
+ DiscussMobileMailboxSelection: require('mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js'),
+ DiscussSidebar: require('mail/static/src/components/discuss_sidebar/discuss_sidebar.js'),
+ MobileMessagingNavbar: require('mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.js'),
+ ModerationDiscardDialog: require('mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js'),
+ ModerationRejectDialog: require('mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js'),
+ NotificationList: require('mail/static/src/components/notification_list/notification_list.js'),
+ ThreadView: require('mail/static/src/components/thread_view/thread_view.js'),
+};
+const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
+const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+
+const patchMixin = require('web.patchMixin');
+
+const { Component } = owl;
+const { useRef } = owl.hooks;
+
+class Discuss extends Component {
+
+ /**
+ * @override
+ */
+ constructor(...args) {
+ super(...args);
+ useShouldUpdateBasedOnProps();
+ useStore((...args) => this._useStoreSelector(...args), {
+ compareDepth: {
+ checkedMessages: 1,
+ uncheckedMessages: 1,
+ },
+ });
+ this._updateLocalStoreProps();
+ /**
+ * Reference of the composer. Useful to focus it.
+ */
+ this._composerRef = useRef('composer');
+ /**
+ * Reference of the ThreadView. Useful to focus it.
+ */
+ this._threadViewRef = useRef('threadView');
+ // bind since passed as props
+ this._onMobileAddItemHeaderInputSelect = this._onMobileAddItemHeaderInputSelect.bind(this);
+ this._onMobileAddItemHeaderInputSource = this._onMobileAddItemHeaderInputSource.bind(this);
+ }
+
+ mounted() {
+ this.discuss.update({ isOpen: true });
+ if (this.discuss.thread) {
+ this.trigger('o-push-state-action-manager');
+ } else if (this.env.isMessagingInitialized()) {
+ this.discuss.openInitThread();
+ }
+ this._updateLocalStoreProps();
+ }
+
+ patched() {
+ this.trigger('o-update-control-panel');
+ if (this.discuss.thread) {
+ this.trigger('o-push-state-action-manager');
+ }
+ if (
+ this.discuss.thread &&
+ this.discuss.thread === this.env.messaging.inbox &&
+ this.discuss.threadView &&
+ this._lastThreadCache === this.discuss.threadView.threadCache.localId &&
+ this._lastThreadCounter > 0 && this.discuss.thread.counter === 0
+ ) {
+ this.trigger('o-show-rainbow-man');
+ }
+ this._activeThreadCache = this.discuss.threadView && this.discuss.threadView.threadCache;
+ this._updateLocalStoreProps();
+ }
+
+ willUnmount() {
+ if (this.discuss) {
+ this.discuss.close();
+ }
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {string}
+ */
+ get addChannelInputPlaceholder() {
+ return this.env._t("Create or search channel...");
+ }
+
+ /**
+ * @returns {string}
+ */
+ get addChatInputPlaceholder() {
+ return this.env._t("Search user...");
+ }
+
+ /**
+ * @returns {mail.discuss}
+ */
+ get discuss() {
+ return this.env.messaging && this.env.messaging.discuss;
+ }
+
+ /**
+ * @returns {Object[]}
+ */
+ mobileNavbarTabs() {
+ return [{
+ icon: 'fa fa-inbox',
+ id: 'mailbox',
+ label: this.env._t("Mailboxes"),
+ }, {
+ icon: 'fa fa-user',
+ id: 'chat',
+ label: this.env._t("Chat"),
+ }, {
+ icon: 'fa fa-users',
+ id: 'channel',
+ label: this.env._t("Channel"),
+ }];
+ }
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _updateLocalStoreProps() {
+ /**
+ * Locally tracked store props `activeThreadCache`.
+ * Useful to set scroll position from last stored one and to display
+ * rainbox man on inbox.
+ */
+ this._lastThreadCache = (
+ this.discuss.threadView &&
+ this.discuss.threadView.threadCache &&
+ this.discuss.threadView.threadCache.localId
+ );
+ /**
+ * Locally tracked store props `threadCounter`.
+ * Useful to display the rainbow man on inbox.
+ */
+ this._lastThreadCounter = (
+ this.discuss.thread &&
+ this.discuss.thread.counter
+ );
+ }
+
+ /**
+ * Returns data selected from the store.
+ *
+ * @private
+ * @param {Object} props
+ * @returns {Object}
+ */
+ _useStoreSelector(props) {
+ const discuss = this.env.messaging && this.env.messaging.discuss;
+ const thread = discuss && discuss.thread;
+ const threadView = discuss && discuss.threadView;
+ const replyingToMessage = discuss && discuss.replyingToMessage;
+ const replyingToMessageOriginThread = replyingToMessage && replyingToMessage.originThread;
+ const checkedMessages = threadView ? threadView.checkedMessages : [];
+ return {
+ checkedMessages,
+ checkedMessagesIsModeratedByCurrentPartner: checkedMessages && checkedMessages.some(message => message.isModeratedByCurrentPartner), // for widget
+ discuss,
+ discussActiveId: discuss && discuss.activeId, // for widget
+ discussActiveMobileNavbarTabId: discuss && discuss.activeMobileNavbarTabId,
+ discussHasModerationDiscardDialog: discuss && discuss.hasModerationDiscardDialog,
+ discussHasModerationRejectDialog: discuss && discuss.hasModerationRejectDialog,
+ discussIsAddingChannel: discuss && discuss.isAddingChannel,
+ discussIsAddingChat: discuss && discuss.isAddingChat,
+ discussIsDoFocus: discuss && discuss.isDoFocus,
+ discussReplyingToMessageOriginThreadComposer: replyingToMessageOriginThread && replyingToMessageOriginThread.composer,
+ inbox: this.env.messaging.inbox,
+ isDeviceMobile: this.env.messaging && this.env.messaging.device.isMobile,
+ isMessagingInitialized: this.env.isMessagingInitialized(),
+ replyingToMessage,
+ starred: this.env.messaging.starred, // for widget
+ thread,
+ threadCache: threadView && threadView.threadCache,
+ threadChannelType: thread && thread.channel_type, // for widget
+ threadDisplayName: thread && thread.displayName, // for widget
+ threadCounter: thread && thread.counter,
+ threadModel: thread && thread.model,
+ threadPublic: thread && thread.public, // for widget
+ threadView,
+ threadViewMessagesLength: threadView && threadView.messages.length, // for widget
+ uncheckedMessages: threadView ? threadView.uncheckedMessages : [], // for widget
+ };
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onDialogClosedModerationDiscard() {
+ this.discuss.update({ hasModerationDiscardDialog: false });
+ }
+
+ /**
+ * @private
+ */
+ _onDialogClosedModerationReject() {
+ this.discuss.update({ hasModerationRejectDialog: false });
+ }
+
+ /**
+ * @private
+ * @param {CustomEvent} ev
+ */
+ _onFocusinComposer(ev) {
+ this.discuss.update({ isDoFocus: false });
+ }
+
+ /**
+ * @private
+ * @param {CustomEvent} ev
+ */
+ _onHideMobileAddItemHeader(ev) {
+ ev.stopPropagation();
+ this.discuss.clearIsAddingItem();
+ }
+
+ /**
+ * @private
+ * @param {Event} ev
+ * @param {Object} ui
+ * @param {Object} ui.item
+ * @param {integer} ui.item.id
+ */
+ _onMobileAddItemHeaderInputSelect(ev, ui) {
+ const discuss = this.discuss;
+ if (discuss.isAddingChannel) {
+ discuss.handleAddChannelAutocompleteSelect(ev, ui);
+ } else {
+ discuss.handleAddChatAutocompleteSelect(ev, ui);
+ }
+ }
+
+ /**
+ * @private
+ * @param {Object} req
+ * @param {string} req.term
+ * @param {function} res
+ */
+ _onMobileAddItemHeaderInputSource(req, res) {
+ if (this.discuss.isAddingChannel) {
+ this.discuss.handleAddChannelAutocompleteSource(req, res);
+ } else {
+ this.discuss.handleAddChatAutocompleteSource(req, res);
+ }
+ }
+
+ /**
+ * @private
+ */
+ _onReplyingToMessageMessagePosted() {
+ this.env.services['notification'].notify({
+ message: _.str.sprintf(
+ this.env._t(`Message posted on "%s"`),
+ owl.utils.escape(this.discuss.replyingToMessage.originThread.displayName)
+ ),
+ type: 'warning',
+ });
+ this.discuss.clearReplyingToMessage();
+ }
+
+ /**
+ * @private
+ * @param {CustomEvent} ev
+ * @param {Object} ev.detail
+ * @param {string} ev.detail.tabId
+ */
+ _onSelectMobileNavbarTab(ev) {
+ ev.stopPropagation();
+ if (this.discuss.activeMobileNavbarTabId === ev.detail.tabId) {
+ return;
+ }
+ this.discuss.clearReplyingToMessage();
+ this.discuss.update({ activeMobileNavbarTabId: ev.detail.tabId });
+ }
+
+ /**
+ * @private
+ * @param {CustomEvent} ev
+ */
+ _onThreadRendered(ev) {
+ this.trigger('o-update-control-panel');
+ }
+
+}
+
+Object.assign(Discuss, {
+ components,
+ props: {},
+ template: 'mail.Discuss',
+});
+
+return patchMixin(Discuss);
+
+});
diff --git a/addons/mail/static/src/components/discuss/discuss.scss b/addons/mail/static/src/components/discuss/discuss.scss
new file mode 100644
index 00000000..5cddf101
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/discuss.scss
@@ -0,0 +1,114 @@
+// ------------------------------------------------------------------
+// Layout
+// ------------------------------------------------------------------
+
+.o_action_manager {
+ // bug with scrollable inside discuss mobile without this...
+ min-height: 0;
+}
+
+.o-autogrow {
+ flex: 1 1 auto;
+}
+
+.o_Discuss {
+ display: flex;
+ height: 100%;
+ min-height: 0;
+
+ &.o-mobile {
+ flex-flow: column;
+ align-items: center;
+ }
+}
+
+.o_Discuss_chatWindowHeader {
+ width: 100%;
+ flex: 0 0 auto;
+}
+
+.o_Discuss_content {
+ height: 100%;
+ overflow: auto;
+ flex: 1 1 auto;
+ display: flex;
+ flex-flow: column;
+}
+
+.o_Discuss_messagingNotInitialized {
+ flex: 1 1 auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.o_Discuss_messagingNotInitializedIcon {
+ margin-right: 3px;
+}
+
+.o_Discuss_mobileAddItemHeader {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ padding: 0 10px;
+}
+
+.o_Discuss_mobileAddItemHeaderInput {
+ flex: 1 1 auto;
+ margin-bottom: 8px;
+ padding: 8px;
+}
+
+.o_Discuss_mobileMailboxSelection {
+ width: 100%;
+}
+
+.o_Discuss_mobileNavbar {
+ width: 100%;
+}
+
+.o_Discuss_noThread {
+ display: flex;
+ flex: 1 1 auto;
+ width: 100%;
+ align-items: center;
+ justify-content: center;
+}
+
+.o_Discuss_replyingToMessageComposer {
+ width: 100%;
+}
+
+.o_Discuss_sidebar {
+ height: 100%;
+ overflow: auto;
+ padding-top: 10px;
+ flex: 0 0 auto;
+}
+
+.o_Discuss_thread {
+ flex: 1 1 auto;
+
+ &.o-mobile {
+ width: 100%;
+ }
+}
+
+.o_Discuss_notificationList {
+ width: 100%;
+ flex: 1 1 auto;
+}
+// ------------------------------------------------------------------
+// Style
+// ------------------------------------------------------------------
+
+.o_Discuss.o-mobile {
+ background-color: white;
+}
+
+.o_Discuss_mobileAddItemHeaderInput {
+ appearance: none;
+ border: 1px solid gray('400');
+ border-radius: 5px;
+ outline: none;
+}
diff --git a/addons/mail/static/src/components/discuss/discuss.xml b/addons/mail/static/src/components/discuss/discuss.xml
new file mode 100644
index 00000000..ad9171a5
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/discuss.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-name="mail.Discuss" owl="1">
+ <div class="o_Discuss"
+ t-att-class="{
+ 'o-adding-item': discuss ? discuss.isAddingChannel or discuss.isAddingChat : false,
+ 'o-mobile': env.messaging ? env.messaging.device.isMobile : false,
+ }"
+ >
+ <t t-if="!env.isMessagingInitialized()">
+ <div class="o_Discuss_messagingNotInitialized"><i class="o_Discuss_messagingNotInitializedIcon fa fa-spinner fa-spin"/>Please wait...</div>
+ </t>
+ <t t-else="">
+ <t t-if="!env.messaging.device.isMobile">
+ <DiscussSidebar class="o_Discuss_sidebar"/>
+ </t>
+ <t t-if="env.messaging.device.isMobile" t-call="mail.Discuss.content"/>
+ <t t-else="">
+ <div class="o_Discuss_content">
+ <t t-call="mail.Discuss.content"/>
+ </div>
+ </t>
+ <t t-if="discuss.hasModerationDiscardDialog">
+ <ModerationDiscardDialog messageLocalIds="discuss.threadView.checkedMessages.map(message => message.localId)" t-on-dialog-closed="_onDialogClosedModerationDiscard"/>
+ </t>
+ <t t-if="discuss.hasModerationRejectDialog">
+ <ModerationRejectDialog messageLocalIds="discuss.threadView.checkedMessages.map(message => message.localId)" t-on-dialog-closed="_onDialogClosedModerationReject"/>
+ </t>
+ </t>
+ </div>
+ </t>
+
+ <t t-name="mail.Discuss.content" owl="1">
+ <t t-if="env.messaging.device.isMobile and discuss.activeMobileNavbarTabId === 'mailbox'">
+ <DiscussMobileMailboxSelection class="o_Discuss_mobileMailboxSelection"/>
+ </t>
+ <t t-if="env.messaging.device.isMobile and (discuss.isAddingChannel or discuss.isAddingChat)">
+ <div class="o_Discuss_mobileAddItemHeader">
+ <AutocompleteInput
+ class="o_Discuss_mobileAddItemHeaderInput"
+ isFocusOnMount="true"
+ isHtml="discuss.isAddingChannel"
+ placeholder="discuss.isAddingChannel ? addChannelInputPlaceholder : addChatInputPlaceholder"
+ select="_onMobileAddItemHeaderInputSelect"
+ source="_onMobileAddItemHeaderInputSource"
+ t-on-o-hide="_onHideMobileAddItemHeader"
+ />
+ </div>
+ </t>
+ <t t-if="discuss.threadView">
+ <ThreadView
+ class="o_Discuss_thread"
+ t-att-class="{ 'o-mobile': env.messaging.device.isMobile }"
+ composerAttachmentsDetailsMode="'card'"
+ hasComposer="discuss.thread.model !== 'mail.box'"
+ hasComposerCurrentPartnerAvatar="!env.messaging.device.isMobile"
+ hasComposerThreadTyping="true"
+ hasMessageCheckbox="true"
+ hasSquashCloseMessages="discuss.thread.model !== 'mail.box'"
+ haveMessagesMarkAsReadIcon="discuss.thread === env.messaging.inbox"
+ haveMessagesReplyIcon="discuss.thread === env.messaging.inbox"
+ isDoFocus="discuss.isDoFocus"
+ selectedMessageLocalId="discuss.replyingToMessage and discuss.replyingToMessage.localId"
+ threadViewLocalId="discuss.threadView.localId"
+ t-on-o-focusin-composer="_onFocusinComposer"
+ t-on-o-rendered="_onThreadRendered"
+ t-ref="threadView"
+ />
+ </t>
+ <t t-if="!discuss.thread and (!env.messaging.device.isMobile or discuss.activeMobileNavbarTabId === 'mailbox')">
+ <div class="o_Discuss_noThread">
+ No conversation selected.
+ </div>
+ </t>
+ <t t-if="env.messaging.device.isMobile and discuss.activeMobileNavbarTabId !== 'mailbox'">
+ <NotificationList
+ class="o_Discuss_notificationList"
+ filter="discuss.activeMobileNavbarTabId"
+ />
+ </t>
+ <t t-if="env.messaging.device.isMobile and !discuss.isReplyingToMessage">
+ <MobileMessagingNavbar
+ class="o_Discuss_mobileNavbar"
+ activeTabId="discuss.activeMobileNavbarTabId"
+ tabs="mobileNavbarTabs()"
+ t-on-o-select-mobile-messaging-navbar-tab="_onSelectMobileNavbarTab"
+ />
+ </t>
+ <t t-if="discuss.isReplyingToMessage">
+ <Composer
+ class="o_Discuss_replyingToMessageComposer"
+ composerLocalId="discuss.replyingToMessage.originThread.composer.localId"
+ hasCurrentPartnerAvatar="!env.messaging.device.isMobile"
+ hasDiscardButton="true"
+ hasThreadName="true"
+ isDoFocus="discuss.isDoFocus"
+ textInputSendShortcuts="['ctrl-enter', 'meta-enter']"
+ t-on-o-focusin-composer="_onFocusinComposer"
+ t-on-o-message-posted="_onReplyingToMessageMessagePosted"
+ t-ref="composer"
+ />
+ </t>
+ </t>
+
+</templates>
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js
new file mode 100644
index 00000000..26431207
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js
@@ -0,0 +1,408 @@
+odoo.define('mail/static/src/components/discuss/tests/discuss_domain_tests.js', function (require) {
+'use strict';
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('discuss', {}, function () {
+QUnit.module('discuss_domain_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.start = async params => {
+ const { afterEvent, env, widget } = await start(Object.assign({}, params, {
+ autoOpenDiscuss: true,
+ data: this.data,
+ hasDiscuss: true,
+ }));
+ this.afterEvent = afterEvent;
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('discuss should filter messages based on given domain', async function (assert) {
+ assert.expect(2);
+
+ this.data['mail.message'].records.push({
+ body: "test",
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ }, {
+ body: "not empty",
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ });
+ await this.start();
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 2,
+ "should have 2 messages in Inbox initially"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ // simulate control panel search
+ this.env.messaging.discuss.update({
+ stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]),
+ });
+ },
+ message: "should wait until search filter is applied",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ hint.data.fetchedMessages.length === 1 &&
+ threadViewer.thread.model === 'mail.box' &&
+ threadViewer.thread.id === 'inbox'
+ );
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should only have the 1 message containing 'test' remaining after doing a search"
+ );
+});
+
+QUnit.test('discuss should keep filter domain on changing thread', async function (assert) {
+ assert.expect(3);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ this.data['mail.message'].records.push({
+ body: "test",
+ channel_ids: [20],
+ }, {
+ body: "not empty",
+ channel_ids: [20],
+ });
+ await this.start();
+ const channel = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should have no message in Inbox initially"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ // simulate control panel search
+ this.env.messaging.discuss.update({
+ stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]),
+ });
+ },
+ message: "should wait until search filter is applied",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.box' &&
+ threadViewer.thread.id === 'inbox'
+ );
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should have still no message in Inbox after doing a search"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${channel.localId}"]
+ `).click();
+ },
+ message: "should wait until channel 20 is loaded after clicking on it",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20
+ );
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should only have the 1 message containing 'test' in channel 20 (due to the domain still applied on changing thread)"
+ );
+});
+
+QUnit.test('discuss should refresh filtered thread on receiving new message', async function (assert) {
+ assert.expect(2);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const channel = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${channel.localId}"]
+ `).click();
+ },
+ message: "should wait until channel 20 is loaded after clicking on it",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20
+ );
+ },
+ });
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ // simulate control panel search
+ this.env.messaging.discuss.update({
+ stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]),
+ });
+ },
+ message: "should wait until search filter is applied",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20
+ );
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should have initially no message in channel 20 matching the search 'test'"
+ );
+
+ // simulate receiving a message
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => this.env.services.rpc({
+ route: '/mail/chat_post',
+ params: {
+ message_content: "test",
+ uuid: channel.uuid,
+ },
+ }),
+ message: "should wait until channel 20 refreshed its filtered message list",
+ predicate: data => {
+ return (
+ data.threadViewer.thread.model === 'mail.channel' &&
+ data.threadViewer.thread.id === 20 &&
+ data.hint.type === 'messages-loaded'
+ );
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should only have the 1 message containing 'test' in channel 20 after just receiving it"
+ );
+});
+
+QUnit.test('discuss should refresh filtered thread on changing thread', async function (assert) {
+ assert.expect(4);
+
+ this.data['mail.channel'].records.push({ id: 20 }, { id: 21 });
+ await this.start();
+ const channel20 = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const channel21 = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 21,
+ model: 'mail.channel',
+ });
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${channel20.localId}"]
+ `).click();
+ },
+ message: "should wait until channel 20 is loaded after clicking on it",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20
+ );
+ },
+ });
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ // simulate control panel search
+ this.env.messaging.discuss.update({
+ stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]),
+ });
+ },
+ message: "should wait until search filter is applied",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20
+ );
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should have initially no message in channel 20 matching the search 'test'"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${channel21.localId}"]
+ `).click();
+ },
+ message: "should wait until channel 21 is loaded after clicking on it",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 21
+ );
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should have no message in channel 21 matching the search 'test'"
+ );
+ // simulate receiving a message on channel 20 while channel 21 is displayed
+ await this.env.services.rpc({
+ route: '/mail/chat_post',
+ params: {
+ message_content: "test",
+ uuid: channel20.uuid,
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should still have no message in channel 21 matching the search 'test' after receiving a message on channel 20"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${channel20.localId}"]
+ `).click();
+ },
+ message: "should wait until channel 20 is loaded with the new message after clicking on it",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20 &&
+ threadViewer.threadCache.fetchedMessages.length === 1
+ );
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should now have the 1 message containing 'test' in channel 20 when displaying it, after having received the message while the channel was not visible"
+ );
+});
+
+QUnit.test('select all and unselect all buttons should work on filtered thread', async function (assert) {
+ assert.expect(4);
+
+ this.data['mail.channel'].records.push({
+ id: 20,
+ is_moderator: true,
+ moderation: true,
+ name: "general",
+ });
+ this.data['mail.message'].records.push({
+ body: "<p>test</p>",
+ model: 'mail.channel',
+ moderation_status: 'pending_moderation',
+ res_id: 20,
+ });
+ await this.start();
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${this.env.messaging.moderation.localId}"]
+ `).click();
+ },
+ message: "should wait until moderation box is loaded after clicking on it",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.box' &&
+ threadViewer.thread.id === 'moderation'
+ );
+ },
+ });
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ // simulate control panel search
+ this.env.messaging.discuss.update({
+ stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]),
+ });
+ },
+ message: "should wait until search filter is applied",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.box' &&
+ threadViewer.thread.id === 'moderation'
+ );
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should only have the 1 message containing 'test' in moderation box"
+ );
+ assert.notOk(
+ document.querySelector('.o_Message_checkbox').checked,
+ "the moderation checkbox should not be checked initially"
+ );
+
+ await afterNextRender(() => document.querySelector('.o_widget_Discuss_controlPanelButtonSelectAll').click());
+ assert.ok(
+ document.querySelector('.o_Message_checkbox').checked,
+ "the moderation checkbox should be checked after clicking on 'select all'"
+ );
+
+ await afterNextRender(() => document.querySelector('.o_widget_Discuss_controlPanelButtonUnselectAll').click());
+ assert.notOk(
+ document.querySelector('.o_Message_checkbox').checked,
+ "the moderation checkbox should be unchecked after clicking on 'unselect all'"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js
new file mode 100644
index 00000000..63b524eb
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js
@@ -0,0 +1,725 @@
+odoo.define('mail/static/src/components/discuss/tests/discuss_inbox_tests.js', function (require) {
+'use strict';
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ nextAnimationFrame,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+const Bus = require('web.Bus');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('discuss', {}, function () {
+QUnit.module('discuss_inbox_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ autoOpenDiscuss: true,
+ data: this.data,
+ hasDiscuss: true,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('reply: discard on pressing escape', async function (assert) {
+ assert.expect(9);
+
+ // partner expected to be found by mention
+ this.data['res.partner'].records.push({
+ email: "testpartnert@odoo.com",
+ id: 11,
+ name: "TestPartner",
+ });
+ // message expected to be found in inbox
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ model: 'res.partner',
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ res_id: 20,
+ });
+ await this.start();
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should display a single message"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Message_commandReply').click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Composer',
+ "should have composer after clicking on reply to message"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`.o_Composer_buttonEmojis`).click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_EmojisPopover',
+ "emojis popover 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_EmojisPopover',
+ "emojis popover 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 afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "@");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "T");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "e");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "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_ComposerSuggestion',
+ "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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ model: 'res.partner',
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ res_id: 20,
+ });
+ await this.start();
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should display a single message"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Message_commandReply').click()
+ );
+ 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 afterNextRender(() =>
+ document.querySelector(`.o_Composer_buttonDiscard`).click()
+ );
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ model: 'res.partner',
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ res_id: 20,
+ });
+ await this.start();
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should display a single message"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Message_commandReply').click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Composer',
+ "should have composer after clicking on reply to message"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`.o_Message_commandReply`).click()
+ );
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ model: 'res.partner',
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ res_id: 20,
+ });
+ await this.start();
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should display a single message"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Message_commandReply').click()
+ );
+ 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 afterNextRender(() =>
+ document.querySelector(`.o_Composer_buttonEmojis`).click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_EmojisPopover',
+ "emojis popover should be opened after clicking on emojis button"
+ );
+
+ await afterNextRender(() => {
+ document.querySelector(`.o_EmojisPopover_emoji`).click();
+ });
+ assert.containsNone(
+ document.body,
+ '.o_EmojisPopover',
+ "emojis popover 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 afterNextRender(() =>
+ document.querySelector(`.o_Message`).click()
+ );
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ is_discussion: false,
+ model: 'res.partner',
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ res_id: 20,
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_post') {
+ assert.step('message_post');
+ assert.strictEqual(
+ args.kwargs.message_type,
+ "comment",
+ "should set message type as 'comment'"
+ );
+ assert.strictEqual(
+ args.kwargs.subtype_xmlid,
+ "mail.mt_note",
+ "should set subtype_xmlid as 'note'"
+ );
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should display a single message"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Message_commandReply').click()
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Composer_buttonSend').textContent.trim(),
+ "Log",
+ "Send button text should be 'Log'"
+ );
+
+ await afterNextRender(() =>
+ document.execCommand('insertText', false, "Test")
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_Composer_buttonSend').click()
+ );
+ assert.verifySteps(['message_post']);
+});
+
+QUnit.test('"reply to" composer should send message if message replied to is not a note', async function (assert) {
+ assert.expect(6);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ is_discussion: true,
+ model: 'res.partner',
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ res_id: 20,
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_post') {
+ assert.step('message_post');
+ assert.strictEqual(
+ args.kwargs.message_type,
+ "comment",
+ "should set message type as 'comment'"
+ );
+ assert.strictEqual(
+ args.kwargs.subtype_xmlid,
+ "mail.mt_comment",
+ "should set subtype_xmlid as 'comment'"
+ );
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should display a single message"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Message_commandReply').click()
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Composer_buttonSend').textContent.trim(),
+ "Send",
+ "Send button text should be 'Send'"
+ );
+
+ await afterNextRender(() =>
+ document.execCommand('insertText', false, "Test")
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_Composer_buttonSend').click()
+ );
+ assert.verifySteps(['message_post']);
+});
+
+QUnit.test('error notifications should not be shown in Inbox', async function (assert) {
+ assert.expect(3);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ id: 100,
+ model: 'mail.channel',
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ res_id: 20,
+ });
+ this.data['mail.notification'].records.push({
+ mail_message_id: 100, // id of related message
+ res_partner_id: this.data.currentPartnerId, // must be for current partner
+ notification_status: 'exception',
+ notification_type: 'email',
+ });
+ await this.start();
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ model: 'mail.channel', // random existing model
+ needaction: true, // message_fetch domain
+ needaction_partner_ids: [this.data.currentPartnerId], // not needed, for consistency
+ subject: "Salutations, voyageur", // will be asserted in the test
+ });
+ await this.start();
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ history_partner_ids: [3], // not needed, for consistency
+ model: 'mail.channel', // random existing model
+ subject: "Salutations, voyageur", // will be asserted in the test
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.box_history',
+ },
+ },
+ });
+ 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 bus = new Bus();
+ bus.on('do-action', null, payload => {
+ // 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(
+ payload.action.type,
+ 'ir.actions.act_window',
+ "action should open a view"
+ );
+ assert.deepEqual(
+ payload.action.views,
+ [[false, 'form']],
+ "action should open form view"
+ );
+ assert.strictEqual(
+ payload.action.res_model,
+ 'some.model',
+ "action should open view with model 'some.model' (model of message origin thread)"
+ );
+ assert.strictEqual(
+ payload.action.res_id,
+ 10,
+ "action should open view with id 10 (id of message origin thread)"
+ );
+ });
+ this.data['some.model'] = { fields: {}, records: [{ id: 10 }] };
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ model: 'some.model',
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ record_name: "Some record",
+ res_id: 10,
+ });
+ await this.start({
+ env: {
+ bus,
+ },
+ });
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [100],
+ model: 'mail.channel',
+ res_id: 100,
+ needaction: true,
+ subject: "Salutations, voyageur",
+ });
+ this.data['mail.channel'].records.push({
+ id: 100,
+ name: "Salutations, voyageur",
+ });
+ await this.start();
+
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [100],
+ model: 'mail.channel',
+ res_id: 100,
+ needaction: true,
+ subject: "Re: Salutations, voyageur",
+ });
+ this.data['mail.channel'].records.push({
+ id: 100,
+ name: "Re: Salutations, voyageur",
+ });
+ await this.start();
+
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [100],
+ model: 'mail.channel',
+ res_id: 100,
+ needaction: true,
+ subject: "Re: Salutations, voyageur",
+ });
+ this.data['mail.channel'].records.push({
+ id: 100,
+ name: "Salutations, voyageur",
+ });
+ await this.start();
+
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [100],
+ model: 'mail.channel',
+ res_id: 100,
+ needaction: true,
+ subject: "Fw: Re: Salutations, voyageur",
+ });
+ this.data['mail.channel'].records.push({
+ id: 100,
+ name: "Salutations, voyageur",
+ });
+ await this.start();
+
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [100],
+ model: 'mail.channel',
+ res_id: 100,
+ needaction: true,
+ subject: "Salutations, voyageur",
+ });
+ this.data['mail.channel'].records.push({
+ id: 100,
+ name: "Re: Salutations, voyageur",
+ });
+ await this.start();
+
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [100],
+ model: 'mail.channel',
+ res_id: 100,
+ needaction: true,
+ subject: "fw: re: Salutations, voyageur",
+ });
+ this.data['mail.channel'].records.push({
+ id: 100,
+ name: "Re: Salutations, voyageur",
+ });
+ await this.start();
+
+ 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);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [100],
+ model: 'mail.channel',
+ res_id: 100,
+ needaction: true,
+ subject: "Re: Re: Salutations, voyageur",
+ });
+ this.data['mail.channel'].records.push({
+ id: 100,
+ name: "Salutations, voyageur",
+ });
+ await this.start();
+
+ assert.containsNone(
+ document.body,
+ '.o_Message_subject',
+ "should not display subject when subject differs from thread name only by the 'Re: Re:'' prefix"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js
new file mode 100644
index 00000000..c4c53cb5
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js
@@ -0,0 +1,1180 @@
+odoo.define('mail/static/src/components/discuss/tests/discuss_moderation_tests.js', function (require) {
+'use strict';
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('discuss', {}, function () {
+QUnit.module('discuss_moderation_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ autoOpenDiscuss: true,
+ data: this.data,
+ hasDiscuss: true,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('as moderator, moderated channel with pending moderation message', async function (assert) {
+ assert.expect(37);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ is_moderator: true, // current user is expected to be moderator of channel
+ moderation: true, // for consistency, but not used in the scope of this test
+ name: "general", // random name, will be asserted in the test
+ });
+ this.data['mail.message'].records.push({
+ body: "<p>test</p>", // random body, will be asserted in the test
+ model: 'mail.channel', // expected value to link message to channel
+ moderation_status: 'pending_moderation', // message is expected to be pending moderation
+ res_id: 20, // id of the channel
+ });
+ await this.start();
+
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.moderation.localId
+ }"]
+ `),
+ "should display the moderation box in the sidebar"
+ );
+ const mailboxCounter = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.moderation.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `);
+ assert.ok(
+ mailboxCounter,
+ "there should be a counter next to the moderation mailbox in the sidebar"
+ );
+ assert.strictEqual(
+ mailboxCounter.textContent.trim(),
+ "1",
+ "the mailbox counter of the moderation mailbox should display '1'"
+ );
+
+ // 1. go to moderation mailbox
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.moderation.localId
+ }"]
+ `).click()
+ );
+ // check message
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should be only one message in moderation box"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Message_content').textContent,
+ "test",
+ "this message pending moderation should have the correct content"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message_originThreadLink',
+ "thee message should have one origin"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Message_originThreadLink').textContent,
+ "#general",
+ "the message pending moderation should have correct origin as its linked document"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message_checkbox',
+ "there should be a moderation checkbox next to the message"
+ );
+ assert.notOk(
+ document.querySelector('.o_Message_checkbox').checked,
+ "the moderation checkbox should be unchecked by default"
+ );
+ // check select all (enabled) / unselect all (disabled) buttons
+ assert.containsOnce(
+ document.body,
+ '.o_widget_Discuss_controlPanelButtonSelectAll',
+ "there should be a 'Select All' button in the control panel"
+ );
+ assert.doesNotHaveClass(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonSelectAll'),
+ 'disabled',
+ "the 'Select All' button should not be disabled"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_widget_Discuss_controlPanelButtonUnselectAll',
+ "there should be a 'Unselect All' button in the control panel"
+ );
+ assert.hasClass(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonUnselectAll'),
+ 'disabled',
+ "the 'Unselect All' button should be disabled"
+ );
+ // check moderate all buttons (invisible)
+ assert.containsN(
+ document.body,
+ '.o_widget_Discuss_controlPanelButtonModeration',
+ 3,
+ "there should be 3 buttons to moderate selected messages in the control panel"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_widget_Discuss_controlPanelButtonModeration.o-accept',
+ "there should one moderate button to accept messages pending moderation"
+ );
+ assert.isNotVisible(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'),
+ "the moderate button 'Accept' should be invisible by default"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_widget_Discuss_controlPanelButtonModeration.o-reject',
+ "there should one moderate button to reject messages pending moderation"
+ );
+ assert.isNotVisible(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-reject'),
+ "the moderate button 'Reject' should be invisible by default"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_widget_Discuss_controlPanelButtonModeration.o-discard',
+ "there should one moderate button to discard messages pending moderation"
+ );
+ assert.isNotVisible(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-discard'),
+ "the moderate button 'Discard' should be invisible by default"
+ );
+
+ // click on message moderation checkbox
+ await afterNextRender(() => document.querySelector('.o_Message_checkbox').click());
+ assert.ok(
+ document.querySelector('.o_Message_checkbox').checked,
+ "the moderation checkbox should become checked after click"
+ );
+ // check select all (disabled) / unselect all buttons (enabled)
+ assert.hasClass(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonSelectAll'),
+ 'disabled',
+ "the 'Select All' button should be disabled"
+ );
+ assert.doesNotHaveClass(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonUnselectAll'),
+ 'disabled',
+ "the 'Unselect All' button should not be disabled"
+ );
+ // check moderate all buttons updated (visible)
+ assert.isVisible(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'),
+ "the moderate button 'Accept' should be visible"
+ );
+ assert.isVisible(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-reject'),
+ "the moderate button 'Reject' should be visible"
+ );
+ assert.isVisible(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-discard'),
+ "the moderate button 'Discard' should be visible"
+ );
+
+ // test select buttons
+ await afterNextRender(() =>
+ document.querySelector('.o_widget_Discuss_controlPanelButtonUnselectAll').click()
+ );
+ assert.notOk(
+ document.querySelector('.o_Message_checkbox').checked,
+ "the moderation checkbox should become unchecked after click"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_widget_Discuss_controlPanelButtonSelectAll').click()
+ );
+ assert.ok(
+ document.querySelector('.o_Message_checkbox').checked,
+ "the moderation checkbox should become checked again after click"
+ );
+
+ // 2. go to channel 'general'
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click()
+ );
+ // check correct message
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should be only one message in general channel"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message_checkbox',
+ "there should be a moderation checkbox next to the message"
+ );
+ assert.notOk(
+ document.querySelector('.o_Message_checkbox').checked,
+ "the moderation checkbox should not be checked here"
+ );
+ await afterNextRender(() => document.querySelector('.o_Message_checkbox').click());
+ // Don't test moderation actions visibility, since it is similar to moderation box.
+
+ // 3. test discard button
+ await afterNextRender(() =>
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-discard').click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ModerationDiscardDialog',
+ "discard dialog should be open"
+ );
+ // the dialog will be tested separately
+ await afterNextRender(() =>
+ document.querySelector('.o_ModerationDiscardDialog .o-cancel').click()
+ );
+ assert.containsNone(
+ document.body,
+ '.o_ModerationDiscardDialog',
+ "discard dialog should be closed"
+ );
+
+ // 4. test reject button
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_widget_Discuss_controlPanelButtonModeration.o-reject
+ `).click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ModerationRejectDialog',
+ "reject dialog should be open"
+ );
+ // the dialog will be tested separately
+ await afterNextRender(() =>
+ document.querySelector('.o_ModerationRejectDialog .o-cancel').click()
+ );
+ assert.containsNone(
+ document.body,
+ '.o_ModerationRejectDialog',
+ "reject dialog should be closed"
+ );
+
+ // 5. test accept button
+ await afterNextRender(() =>
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept').click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should still be only one message in general channel"
+ );
+ assert.containsNone(
+ document.body,
+ '.o_Message_checkbox',
+ "there should not be a moderation checkbox next to the message"
+ );
+});
+
+QUnit.test('as moderator, accept pending moderation message', async function (assert) {
+ assert.expect(12);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ is_moderator: true, // current user is expected to be moderator of channel
+ moderation: true, // for consistency, but not used in the scope of this test
+ name: "general", // random name, will be asserted in the test
+ });
+ this.data['mail.message'].records.push({
+ body: "<p>test</p>", // random body, will be asserted in the test
+ id: 100, // random unique id, will be asserted during the test
+ model: 'mail.channel', // expected value to link message to channel
+ moderation_status: 'pending_moderation', // message is expected to be pending moderation
+ res_id: 20, // id of the channel
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'moderate') {
+ assert.step('moderate');
+ const messageIDs = args.args[0];
+ const decision = args.args[1];
+ assert.strictEqual(
+ messageIDs.length,
+ 1,
+ "should moderate one message"
+ );
+ assert.strictEqual(
+ messageIDs[0],
+ 100,
+ "should moderate message with ID 100"
+ );
+ assert.strictEqual(
+ decision,
+ 'accept',
+ "should accept the message"
+ );
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ // 1. go to moderation box
+ const moderationBox = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.moderation.localId
+ }"]
+ `);
+ assert.ok(
+ moderationBox,
+ "should display the moderation box"
+ );
+
+ await afterNextRender(() => moderationBox.click());
+ assert.ok(
+ document.querySelector(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `),
+ "should display the message to moderate"
+ );
+ const acceptButton = document.querySelector(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ .o_Message_moderationAction.o-accept
+ `);
+ assert.ok(acceptButton, "should display the accept button");
+
+ await afterNextRender(() => acceptButton.click());
+ assert.verifySteps(['moderate']);
+ assert.containsOnce(
+ document.body,
+ '.o_MessageList_emptyTitle',
+ "should now have no message displayed in moderation box"
+ );
+
+ // 2. go to channel 'general'
+ const channel = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ assert.ok(
+ channel,
+ "should display the general channel"
+ );
+
+ await afterNextRender(() => channel.click());
+ const message = document.querySelector(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `);
+ assert.ok(
+ message,
+ "should display the accepted message"
+ );
+ assert.containsNone(
+ message,
+ '.o_Message_moderationPending',
+ "the message should not be pending moderation"
+ );
+});
+
+QUnit.test('as moderator, reject pending moderation message (reject with explanation)', async function (assert) {
+ assert.expect(23);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ is_moderator: true, // current user is expected to be moderator of channel
+ moderation: true, // for consistency, but not used in the scope of this test
+ name: "general", // random name, will be asserted in the test
+ });
+ this.data['mail.message'].records.push({
+ body: "<p>test</p>", // random body, will be asserted in the test
+ id: 100, // random unique id, will be asserted during the test
+ model: 'mail.channel', // expected value to link message to channel
+ moderation_status: 'pending_moderation', // message is expected to be pending moderation
+ res_id: 20, // id of the channel
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'moderate') {
+ assert.step('moderate');
+ const messageIDs = args.args[0];
+ const decision = args.args[1];
+ const kwargs = args.kwargs;
+ assert.strictEqual(
+ messageIDs.length,
+ 1,
+ "should moderate one message"
+ );
+ assert.strictEqual(
+ messageIDs[0],
+ 100,
+ "should moderate message with ID 100"
+ );
+ assert.strictEqual(
+ decision,
+ 'reject',
+ "should reject the message"
+ );
+ assert.strictEqual(
+ kwargs.title,
+ "Message Rejected",
+ "should have correct reject message title"
+ );
+ assert.strictEqual(
+ kwargs.comment,
+ "Your message was rejected by moderator.",
+ "should have correct reject message body / comment"
+ );
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ // 1. go to moderation box
+ const moderationBox = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.moderation.localId
+ }"]
+ `);
+ assert.ok(
+ moderationBox,
+ "should display the moderation box"
+ );
+
+ await afterNextRender(() => moderationBox.click());
+ const pendingMessage = document.querySelector(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `);
+ assert.ok(
+ pendingMessage,
+ "should display the message to moderate"
+ );
+ const rejectButton = pendingMessage.querySelector(':scope .o_Message_moderationAction.o-reject');
+ assert.ok(
+ rejectButton,
+ "should display the reject button"
+ );
+
+ await afterNextRender(() => rejectButton.click());
+ const dialog = document.querySelector('.o_ModerationRejectDialog');
+ assert.ok(
+ dialog,
+ "a dialog should be prompt to the moderator on click reject"
+ );
+ assert.strictEqual(
+ dialog.querySelector('.modal-title').textContent,
+ "Send explanation to author",
+ "dialog should have correct title"
+ );
+
+ const messageTitle = dialog.querySelector(':scope .o_ModerationRejectDialog_title');
+ assert.ok(
+ messageTitle,
+ "should have a title for rejecting"
+ );
+ assert.hasAttrValue(
+ messageTitle,
+ 'placeholder',
+ "Subject",
+ "title for reject reason should have correct placeholder"
+ );
+ assert.strictEqual(
+ messageTitle.value,
+ "Message Rejected",
+ "title for reject reason should have correct default value"
+ );
+
+ const messageComment = dialog.querySelector(':scope .o_ModerationRejectDialog_comment');
+ assert.ok(
+ messageComment,
+ "should have a comment for rejecting"
+ );
+ assert.hasAttrValue(
+ messageComment,
+ 'placeholder',
+ "Mail Body",
+ "comment for reject reason should have correct placeholder"
+ );
+ assert.strictEqual(
+ messageComment.value,
+ "Your message was rejected by moderator.",
+ "comment for reject reason should have correct default text content"
+ );
+ const confirmReject = dialog.querySelector(':scope .o-reject');
+ assert.ok(
+ confirmReject,
+ "should have reject button"
+ );
+ assert.strictEqual(
+ confirmReject.textContent,
+ "Reject"
+ );
+
+ await afterNextRender(() => confirmReject.click());
+ assert.verifySteps(['moderate']);
+ assert.containsOnce(
+ document.body,
+ '.o_MessageList_emptyTitle',
+ "should now have no message displayed in moderation box"
+ );
+
+ // 2. go to channel 'general'
+ const channel = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ assert.ok(
+ channel,
+ 'should display the general channel'
+ );
+
+ await afterNextRender(() => channel.click());
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should now have no message in channel"
+ );
+});
+
+QUnit.test('as moderator, discard pending moderation message (reject without explanation)', async function (assert) {
+ assert.expect(16);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ is_moderator: true, // current user is expected to be moderator of channel
+ moderation: true, // for consistency, but not used in the scope of this test
+ name: "general", // random name, will be asserted in the test
+ });
+ this.data['mail.message'].records.push({
+ body: "<p>test</p>", // random body, will be asserted in the test
+ id: 100, // random unique id, will be asserted during the test
+ model: 'mail.channel', // expected value to link message to channel
+ moderation_status: 'pending_moderation', // message is expected to be pending moderation
+ res_id: 20, // id of the channel
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'moderate') {
+ assert.step('moderate');
+ const messageIDs = args.args[0];
+ const decision = args.args[1];
+ assert.strictEqual(messageIDs.length, 1, "should moderate one message");
+ assert.strictEqual(messageIDs[0], 100, "should moderate message with ID 100");
+ assert.strictEqual(decision, 'discard', "should discard the message");
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ // 1. go to moderation box
+ const moderationBox = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.moderation.localId
+ }"]
+ `);
+ assert.ok(
+ moderationBox,
+ "should display the moderation box"
+ );
+
+ await afterNextRender(() => moderationBox.click());
+ const pendingMessage = document.querySelector(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `);
+ assert.ok(
+ pendingMessage,
+ "should display the message to moderate"
+ );
+
+ const discardButton = pendingMessage.querySelector(`
+ :scope .o_Message_moderationAction.o-discard
+ `);
+ assert.ok(
+ discardButton,
+ "should display the discard button"
+ );
+
+ await afterNextRender(() => discardButton.click());
+ const dialog = document.querySelector('.o_ModerationDiscardDialog');
+ assert.ok(
+ dialog,
+ "a dialog should be prompt to the moderator on click discard"
+ );
+ assert.strictEqual(
+ dialog.querySelector('.modal-title').textContent,
+ "Confirmation",
+ "dialog should have correct title"
+ );
+ assert.strictEqual(
+ dialog.textContent,
+ "Confirmation×You are going to discard 1 message.Do you confirm the action?DiscardCancel",
+ "should warn the user on discard action"
+ );
+
+ const confirmDiscard = dialog.querySelector(':scope .o-discard');
+ assert.ok(
+ confirmDiscard,
+ "should have discard button"
+ );
+ assert.strictEqual(
+ confirmDiscard.textContent,
+ "Discard"
+ );
+
+ await afterNextRender(() => confirmDiscard.click());
+ assert.verifySteps(['moderate']);
+ assert.containsOnce(
+ document.body,
+ '.o_MessageList_emptyTitle',
+ "should now have no message displayed in moderation box"
+ );
+
+ // 2. go to channel 'general'
+ const channel = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ assert.ok(
+ channel,
+ "should display the general channel"
+ );
+
+ await afterNextRender(() => channel.click());
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should now have no message in channel"
+ );
+});
+
+QUnit.test('as author, send message in moderated channel', async function (assert) {
+ assert.expect(4);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ moderation: true, // channel must be moderated to test the feature
+ name: "general", // random name, will be asserted in the test
+ });
+ await this.start();
+ const channel = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ assert.ok(
+ channel,
+ "should display the general channel"
+ );
+
+ // go to channel 'general'
+ await afterNextRender(() => channel.click());
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should have no message in channel"
+ );
+
+ // post a message
+ await afterNextRender(() => {
+ const textInput = document.querySelector('.o_ComposerTextInput_textarea');
+ textInput.focus();
+ document.execCommand('insertText', false, "Some Text");
+ });
+ await afterNextRender(() => document.querySelector('.o_Composer_buttonSend').click());
+ const messagePending = document.querySelector('.o_Message_moderationPending');
+ assert.ok(
+ messagePending,
+ "should display the pending message with pending info"
+ );
+ assert.hasClass(
+ messagePending,
+ 'o-author',
+ "the message should be pending moderation as author"
+ );
+});
+
+QUnit.test('as author, sent message accepted in moderated channel', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ moderation: true, // for consistency, but not used in the scope of this test
+ name: "general", // random name, will be asserted in the test
+ });
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ id: 100, // random unique id, will be referenced in the test
+ model: 'mail.channel', // expected value to link message to channel
+ moderation_status: 'pending_moderation', // message is expected to be pending
+ res_id: 20, // id of the channel
+ });
+ await this.start();
+
+ const channel = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ assert.ok(
+ channel,
+ "should display the general channel"
+ );
+
+ await afterNextRender(() => channel.click());
+ const messagePending = document.querySelector(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ .o_Message_moderationPending
+ `);
+ assert.ok(
+ messagePending,
+ "should display the pending message with pending info"
+ );
+ assert.hasClass(
+ messagePending,
+ 'o-author',
+ "the message should be pending moderation as author"
+ );
+
+ // simulate accepted message
+ await afterNextRender(() => {
+ const messageData = {
+ id: 100,
+ moderation_status: 'accepted',
+ };
+ const notification = [[false, 'mail.channel', 20], messageData];
+ this.widget.call('bus_service', 'trigger', 'notification', [notification]);
+ });
+
+ // check message is accepted
+ const message = document.querySelector(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `);
+ assert.ok(
+ message,
+ "should still display the message"
+ );
+ assert.containsNone(
+ message,
+ '.o_Message_moderationPending',
+ "the message should not be in pending moderation anymore"
+ );
+});
+
+QUnit.test('as author, sent message rejected in moderated channel', async function (assert) {
+ assert.expect(4);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ moderation: true, // for consistency, but not used in the scope of this test
+ name: "general", // random name, will be asserted in the test
+ });
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ id: 100, // random unique id, will be referenced in the test
+ model: 'mail.channel', // expected value to link message to channel
+ moderation_status: 'pending_moderation', // message is expected to be pending
+ res_id: 20, // id of the channel
+ });
+ await this.start();
+
+ const channel = document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ assert.ok(
+ channel,
+ "should display the general channel"
+ );
+
+ await afterNextRender(() => channel.click());
+ const messagePending = document.querySelector(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ .o_Message_moderationPending
+ `);
+ assert.ok(
+ messagePending,
+ "should display the pending message with pending info"
+ );
+ assert.hasClass(
+ messagePending,
+ 'o-author',
+ "the message should be pending moderation as author"
+ );
+
+ // simulate reject from moderator
+ await afterNextRender(() => {
+ const notifData = {
+ type: 'deletion',
+ message_ids: [100],
+ };
+ const notification = [[false, 'res.partner', this.env.messaging.currentPartner.id], notifData];
+ this.widget.call('bus_service', 'trigger', 'notification', [notification]);
+ });
+ // check no message
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "message should be removed from channel after reject"
+ );
+});
+
+QUnit.test('as moderator, pending moderation message accessibility', async function (assert) {
+ // pending moderation message should appear in moderation box and in origin thread
+ assert.expect(3);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ is_moderator: true, // current user is expected to be moderator of channel
+ moderation: true, // channel must be moderated to test the feature
+ });
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ id: 100, // random unique id, will be referenced in the test
+ model: 'mail.channel', // expected value to link message to channel
+ moderation_status: 'pending_moderation', // message is expected to be pending
+ res_id: 20, // id of the channel
+ });
+ await this.start();
+
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({ id: 20, model: 'mail.channel' });
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.moderation.localId
+ }"]
+ `),
+ "should display the moderation box in the sidebar"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${thread.localId}"]
+ `).click()
+ );
+ const message = this.env.models['mail.message'].findFromIdentifyingData({ id: 100 });
+ assert.containsOnce(
+ document.body,
+ `.o_Message[data-message-local-id="${message.localId}"]`,
+ "the pending moderation message should be in the channel"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.moderation.localId
+ }"]
+ `).click()
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Message[data-message-local-id="${message.localId}"]`,
+ "the pending moderation message should be in moderation box"
+ );
+});
+
+QUnit.test('as author, pending moderation message should appear in origin thread', async function (assert) {
+ assert.expect(1);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ moderation: true, // channel must be moderated to test the feature
+ });
+ this.data['mail.message'].records.push({
+ author_id: this.data.currentPartnerId, // test as author of message
+ body: "not empty",
+ id: 100, // random unique id, will be referenced in the test
+ model: 'mail.channel', // expected value to link message to channel
+ moderation_status: 'pending_moderation', // message is expected to be pending
+ res_id: 20, // id of the channel
+ });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({ id: 20, model: 'mail.channel' });
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${thread.localId}"]
+ `).click()
+ );
+ const message = this.env.models['mail.message'].findFromIdentifyingData({ id: 100 });
+ assert.containsOnce(
+ document.body,
+ `.o_Message[data-message-local-id="${message.localId}"]`,
+ "the pending moderation message should be in the channel"
+ );
+});
+
+QUnit.test('as moderator, new pending moderation message posted by someone else', async function (assert) {
+ // the message should appear in origin thread and moderation box if I moderate it
+ assert.expect(3);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ is_moderator: true, // current user is expected to be moderator of channel
+ moderation: true, // channel must be moderated to test the feature
+ });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({ id: 20, model: 'mail.channel' });
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${thread.localId}"]
+ `).click()
+ );
+ assert.containsNone(
+ document.body,
+ `.o_Message`,
+ "should have no message in the channel initially"
+ );
+
+ // simulate receiving the message
+ const messageData = {
+ author_id: [10, 'john doe'], // random id, different than current partner
+ body: "not empty",
+ channel_ids: [], // server do NOT return channel_id of the message if pending moderation
+ id: 1, // random unique id
+ model: 'mail.channel', // expected value to link message to channel
+ moderation_status: 'pending_moderation', // message is expected to be pending
+ res_id: 20, // id of the channel
+ };
+ await afterNextRender(() => {
+ const notifications = [[
+ ['my-db', 'res.partner', this.env.messaging.currentPartner.id],
+ { type: 'moderator', message: messageData },
+ ]];
+ this.widget.call('bus_service', 'trigger', 'notification', notifications);
+ });
+ const message = this.env.models['mail.message'].findFromIdentifyingData({ id: 1 });
+ assert.containsOnce(
+ document.body,
+ `.o_Message[data-message-local-id="${message.localId}"]`,
+ "the pending moderation message should be in the channel"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.moderation.localId
+ }"]
+ `).click()
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Message[data-message-local-id="${message.localId}"]`,
+ "the pending moderation message should be in moderation box"
+ );
+});
+
+QUnit.test('accept multiple moderation messages', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ is_moderator: true, // current user is expected to be moderator of channel
+ moderation: true, // channel must be moderated to test the feature
+ });
+ this.data['mail.message'].records.push(
+ {
+ body: "not empty",
+ model: 'mail.channel',
+ moderation_status: 'pending_moderation',
+ res_id: 20,
+ },
+ {
+ body: "not empty",
+ model: 'mail.channel',
+ moderation_status: 'pending_moderation',
+ res_id: 20,
+ },
+ {
+ body: "not empty",
+ model: 'mail.channel',
+ moderation_status: 'pending_moderation',
+ res_id: 20,
+ }
+ );
+
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.box_moderation',
+ },
+ },
+ });
+
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 3,
+ "should initially display 3 messages"
+ );
+
+ await afterNextRender(() => {
+ document.querySelectorAll('.o_Message_checkbox')[0].click();
+ document.querySelectorAll('.o_Message_checkbox')[1].click();
+ });
+ assert.containsN(
+ document.body,
+ '.o_Message_checkbox:checked',
+ 2,
+ "2 messages should have been checked after clicking on their respective checkbox"
+ );
+ assert.doesNotHaveClass(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'),
+ 'o_hidden',
+ "global accept button should be displayed as two messages are selected"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept').click()
+ );
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 1,
+ "should display 1 message as the 2 others have been accepted"
+ );
+ assert.hasClass(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'),
+ 'o_hidden',
+ "global accept button should no longer be displayed as messages have been unselected"
+ );
+});
+
+QUnit.test('accept multiple moderation messages after having accepted other messages', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.channel'].records.push({
+ id: 20, // random unique id, will be used to link message and will be referenced in the test
+ is_moderator: true, // current user is expected to be moderator of channel
+ moderation: true, // channel must be moderated to test the feature
+ });
+ this.data['mail.message'].records.push(
+ {
+ body: "not empty",
+ model: 'mail.channel',
+ moderation_status: 'pending_moderation',
+ res_id: 20,
+ },
+ {
+ body: "not empty",
+ model: 'mail.channel',
+ moderation_status: 'pending_moderation',
+ res_id: 20,
+ },
+ {
+ body: "not empty",
+ model: 'mail.channel',
+ moderation_status: 'pending_moderation',
+ res_id: 20,
+ }
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.box_moderation',
+ },
+ },
+ });
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 3,
+ "should initially display 3 messages"
+ );
+
+ await afterNextRender(() => {
+ document.querySelectorAll('.o_Message_checkbox')[0].click();
+ });
+ await afterNextRender(() =>
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept').click()
+ );
+ await afterNextRender(() => document.querySelectorAll('.o_Message_checkbox')[0].click());
+ assert.containsOnce(
+ document.body,
+ '.o_Message_checkbox:checked',
+ "a message should have been checked after clicking on its checkbox"
+ );
+ assert.doesNotHaveClass(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'),
+ 'o_hidden',
+ "global accept button should be displayed as a message is selected"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept').click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should display only one message left after the two others has been accepted"
+ );
+ assert.hasClass(
+ document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'),
+ 'o_hidden',
+ "global accept button should no longer be displayed as message has been unselected"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js
new file mode 100644
index 00000000..a24ce411
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js
@@ -0,0 +1,238 @@
+odoo.define('mail/static/src/components/discuss/tests/discuss_pinned_tests.js', function (require) {
+'use strict';
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('discuss', {}, function () {
+QUnit.module('discuss_pinned_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ autoOpenDiscuss: true,
+ data: this.data,
+ hasDiscuss: true,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('sidebar: pinned channel 1: init with one pinned channel', async function (assert) {
+ assert.expect(2);
+
+ // channel that is expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
+ "The Inbox is opened in discuss"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]`,
+ "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);
+
+ // channel that is expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+
+ const threadGeneral = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebarItem[data-thread-local-id="${
+ threadGeneral.localId
+ }"]`).click()
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${threadGeneral.localId}"]`,
+ "The channel #General is displayed in discuss"
+ );
+});
+
+QUnit.test('sidebar: pinned channel 3: open pinned channel and unpin it', async function (assert) {
+ assert.expect(8);
+
+ // channel that is expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({
+ id: 20,
+ is_minimized: true,
+ state: 'open',
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'execute_command') {
+ assert.step('execute_command');
+ assert.deepEqual(args.args[0], [20],
+ "The right id is sent to the server to remove"
+ );
+ assert.strictEqual(args.kwargs.command, 'leave',
+ "The right command is sent to the server"
+ );
+ }
+ if (args.method === 'channel_fold') {
+ assert.step('channel_fold');
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ const threadGeneral = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebarItem[data-thread-local-id="${
+ threadGeneral.localId
+ }"]`).click()
+ );
+ assert.verifySteps([], "neither channel_fold nor execute_command are called yet");
+ await afterNextRender(() =>
+ document.querySelector('.o_DiscussSidebarItem_commandLeave').click()
+ );
+ assert.verifySteps(
+ [
+ 'channel_fold',
+ 'execute_command'
+ ],
+ "both channel_fold and execute_command have been called when unpinning a channel"
+ );
+ assert.containsNone(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`,
+ "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);
+
+ // channel that is expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const threadGeneral = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
+ "The Inbox is opened in discuss"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`,
+ "1 channel is present in discuss sidebar and it is 'general'"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebarItem[data-thread-local-id="${
+ threadGeneral.localId
+ }"]`).click()
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${threadGeneral.localId}"]`,
+ "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(() => {
+ const notif = [
+ ["dbName", 'res.partner', this.env.messaging.currentPartner.id],
+ {
+ channel_type: 'channel',
+ id: 20,
+ info: 'unsubscribe',
+ name: "General",
+ public: 'public',
+ state: 'open',
+ }
+ ];
+ this.env.services.bus_service.trigger('notification', [notif]);
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Discuss_noThread',
+ "should have no thread opened in discuss"
+ );
+ assert.containsNone(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`,
+ "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
+
+ // channel that is expected to be found in the sidebar
+ this.data['mail.channel'].records.push({
+ group_based_subscription: true, // expected value for this test
+ id: 20, // random unique id, will be referenced in the test
+ is_pinned: false, // expected value for this test
+ });
+ await this.start();
+ const threadGeneral = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ assert.containsOnce(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`,
+ "The channel #General is in discuss sidebar"
+ );
+ assert.containsNone(
+ document.body,
+ 'o_DiscussSidebarItem_commandLeave',
+ "The group_based_subscription channel is not unpinnable"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_sidebar_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_sidebar_tests.js
new file mode 100644
index 00000000..34c884eb
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/tests/discuss_sidebar_tests.js
@@ -0,0 +1,163 @@
+odoo.define('mail/static/src/components/discuss/tests/discuss_sidebar_tests.js', function (require) {
+'use strict';
+
+const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js');
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ nextAnimationFrame,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('discuss', {}, function () {
+QUnit.module('discuss_sidebar_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ autoOpenDiscuss: true,
+ data: this.data,
+ hasDiscuss: true,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('sidebar find shows channels matching search term', async function (assert) {
+ assert.expect(3);
+
+ this.data['mail.channel'].records.push({
+ channel_partner_ids: [],
+ channel_type: 'channel',
+ id: 20,
+ members: [],
+ name: 'test',
+ public: 'public',
+ });
+ const searchReadDef = makeDeferred();
+ await this.start({
+ async mockRPC(route, args) {
+ const res = await this._super(...arguments);
+ if (args.method === 'search_read') {
+ searchReadDef.resolve();
+ }
+ return res;
+ },
+ });
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebar_groupHeaderItemAdd`).click()
+ );
+ document.querySelector(`.o_DiscussSidebar_itemNew`).focus();
+ document.execCommand('insertText', false, "test");
+ document.querySelector(`.o_DiscussSidebar_itemNew`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_DiscussSidebar_itemNew`)
+ .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 3 lines:
+ // One for the existing channel itself
+ // One for creating a public channel with the search term
+ // One for creating a private channel with the search term
+ 3
+ );
+ 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);
+
+ this.data['mail.channel'].records.push({
+ channel_partner_ids: [this.data.currentPartnerId],
+ channel_type: 'channel',
+ id: 20,
+ members: [this.data.currentPartnerId],
+ name: 'test',
+ public: 'public',
+ });
+ const searchReadDef = makeDeferred();
+ await this.start({
+ async mockRPC(route, args) {
+ const res = await this._super(...arguments);
+ if (args.method === 'search_read') {
+ searchReadDef.resolve();
+ }
+ return res;
+ },
+ });
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebar_groupHeaderItemAdd`).click()
+ );
+ document.querySelector(`.o_DiscussSidebar_itemNew`).focus();
+ document.execCommand('insertText', false, "test");
+ document.querySelector(`.o_DiscussSidebar_itemNew`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_DiscussSidebar_itemNew`)
+ .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 3 lines:
+ // One for the existing channel itself
+ // One for creating a public channel with the search term
+ // One for creating a private channel with the search term
+ 3
+ );
+ 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);
+
+ this.data['mail.channel'].records.push(
+ { id: 19, name: "Xyz" },
+ { id: 20, name: "abc" },
+ { id: 21, name: "Abc" },
+ { id: 22, name: "Xyz" }
+ );
+ await this.start();
+ const results = document.querySelectorAll('.o_DiscussSidebar_groupChannel .o_DiscussSidebarItem_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"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
new file mode 100644
index 00000000..dc2005e5
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
@@ -0,0 +1,4447 @@
+odoo.define('mail/static/src/components/discuss/tests/discuss_tests.js', function (require) {
+'use strict';
+
+const BusService = require('bus.BusService');
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ nextAnimationFrame,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+const Bus = require('web.Bus');
+const { makeTestPromise, file: { createFile, inputFiles } } = require('web.test_utils');
+
+const {
+ applyFilter,
+ toggleAddCustomFilter,
+ toggleFilterMenu,
+} = require('web.test_utils_control_panel');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('discuss', {}, function () {
+QUnit.module('discuss_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.start = async params => {
+ const { afterEvent, env, widget } = await start(Object.assign({}, params, {
+ autoOpenDiscuss: true,
+ data: this.data,
+ hasDiscuss: true,
+ }));
+ this.afterEvent = afterEvent;
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('messaging not initialized', async function (assert) {
+ assert.expect(1);
+
+ await this.start({
+ async mockRPC(route) {
+ const _super = this._super.bind(this, ...arguments); // limitation of class.js
+ if (route === '/mail/init_messaging') {
+ await makeTestPromise(); // simulate messaging never initialized
+ }
+ return _super();
+ },
+ waitUntilMessagingCondition: 'created',
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Discuss_messagingNotInitialized').length,
+ 1,
+ "should display messaging not initialized"
+ );
+});
+
+QUnit.test('messaging becomes initialized', async function (assert) {
+ assert.expect(2);
+
+ const messagingInitializedProm = makeTestPromise();
+
+ await this.start({
+ async mockRPC(route) {
+ const _super = this._super.bind(this, ...arguments); // limitation of class.js
+ if (route === '/mail/init_messaging') {
+ await messagingInitializedProm;
+ }
+ return _super();
+ },
+ waitUntilMessagingCondition: 'created',
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Discuss_messagingNotInitialized').length,
+ 1,
+ "should display messaging not initialized"
+ );
+
+ await afterNextRender(() => messagingInitializedProm.resolve());
+ assert.strictEqual(
+ document.querySelectorAll('.o_Discuss_messagingNotInitialized').length,
+ 0,
+ "should no longer display messaging not initialized"
+ );
+});
+
+QUnit.test('basic rendering', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll('.o_Discuss_sidebar').length,
+ 1,
+ "should have a sidebar section"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Discuss_content').length,
+ 1,
+ "should have content section"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Discuss_thread').length,
+ 1,
+ "should have thread section inside content"
+ );
+ assert.ok(
+ document.querySelector('.o_Discuss_thread').classList.contains('o_ThreadView'),
+ "thread section should use ThreadView component"
+ );
+});
+
+QUnit.test('basic rendering: sidebar', async function (assert) {
+ assert.expect(20);
+
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_group`).length,
+ 3,
+ "should have 3 groups in sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupMailbox`).length,
+ 1,
+ "should have group 'Mailbox' in sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupMailbox .o_DiscussSidebar_groupHeader
+ `).length,
+ 0,
+ "mailbox category should not have any header"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupMailbox .o_DiscussSidebar_item
+ `).length,
+ 3,
+ "should have 3 mailbox items"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupMailbox
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `).length,
+ 1,
+ "should have inbox mailbox item"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupMailbox
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ `).length,
+ 1,
+ "should have starred mailbox item"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupMailbox
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).length,
+ 1,
+ "should have history mailbox item"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_sidebar .o_DiscussSidebar_separator`).length,
+ 1,
+ "should have separator (between mailboxes and channels, but that's not tested)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel`).length,
+ 1,
+ "should have group 'Channel' in sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupChannel .o_DiscussSidebar_groupHeader
+ `).length,
+ 1,
+ "channel category should have a header"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupChannel .o_DiscussSidebar_groupTitle
+ `).length,
+ 1,
+ "should have title in channel header"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel .o_DiscussSidebar_groupTitle
+ `).textContent.trim(),
+ "Channels"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_list`).length,
+ 1,
+ "channel category should list items"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length,
+ 0,
+ "channel category should have no item by default"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChat`).length,
+ 1,
+ "should have group 'Chat' in sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_groupHeader`).length,
+ 1,
+ "channel category should have a header"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_groupTitle`).length,
+ 1,
+ "should have title in chat header"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChat .o_DiscussSidebar_groupTitle
+ `).textContent.trim(),
+ "Direct Messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_list`).length,
+ 1,
+ "chat category should list items"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`).length,
+ 0,
+ "chat category should have no item by default"
+ );
+});
+
+QUnit.test('sidebar: basic mailbox rendering', async function (assert) {
+ assert.expect(6);
+
+ await this.start();
+ const inbox = document.querySelector(`
+ .o_DiscussSidebar_groupMailbox
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `);
+ assert.strictEqual(
+ inbox.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator`).length,
+ 1,
+ "mailbox should have active indicator"
+ );
+ assert.strictEqual(
+ inbox.querySelectorAll(`:scope .o_ThreadIcon`).length,
+ 1,
+ "mailbox should have an icon"
+ );
+ assert.strictEqual(
+ inbox.querySelectorAll(`:scope .o_ThreadIcon_mailboxInbox`).length,
+ 1,
+ "inbox should have 'inbox' icon"
+ );
+ assert.strictEqual(
+ inbox.querySelectorAll(`:scope .o_DiscussSidebarItem_name`).length,
+ 1,
+ "mailbox should have a name"
+ );
+ assert.strictEqual(
+ inbox.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent,
+ "Inbox",
+ "inbox should have name 'Inbox'"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).length,
+ 0,
+ "should have no counter when equal to 0 (default value)"
+ );
+});
+
+QUnit.test('sidebar: default active inbox', async function (assert) {
+ assert.expect(1);
+
+ await this.start();
+ const inbox = document.querySelector(`
+ .o_DiscussSidebar_groupMailbox
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `);
+ assert.ok(
+ inbox.querySelector(`
+ :scope .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "inbox should be active by default"
+ );
+});
+
+QUnit.test('sidebar: change item', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "inbox should be active by default"
+ );
+ assert.notOk(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "starred should be inactive by default"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ `).click()
+ );
+ assert.notOk(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "inbox mailbox should become inactive"
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "starred mailbox should become active");
+});
+
+QUnit.test('sidebar: inbox with counter', async function (assert) {
+ assert.expect(2);
+
+ // notification expected to be counted at init_messaging
+ this.data['mail.notification'].records.push({ res_partner_id: this.data.currentPartnerId });
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).length,
+ 1,
+ "should display a counter (= have a counter when different from 0)"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).textContent,
+ "1",
+ "should have counter value"
+ );
+});
+
+QUnit.test('sidebar: add channel', async function (assert) {
+ assert.expect(3);
+
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_groupHeaderItemAdd
+ `).length,
+ 1,
+ "should be able to add channel from header"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_groupHeaderItemAdd
+ `).title,
+ "Add or join a channel");
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel .o_DiscussSidebar_groupHeaderItemAdd
+ `).click()
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_itemNew`).length,
+ 1,
+ "should have item to add a new channel"
+ );
+});
+
+QUnit.test('sidebar: basic channel rendering', async function (assert) {
+ assert.expect(14);
+
+ // channel expected to be found in the sidebar,
+ // with a random unique id and name that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20, name: "General" });
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length,
+ 1,
+ "should have one channel item");
+ let channel = document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item
+ `);
+ assert.strictEqual(
+ channel.dataset.threadLocalId,
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId,
+ "should have channel with Id 20"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator`).length,
+ 1,
+ "should have active indicator"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator.o-item-active`).length,
+ 0,
+ "should not be active by default"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_ThreadIcon`).length,
+ 1,
+ "should have an icon"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_name`).length,
+ 1,
+ "should have a name"
+ );
+ assert.strictEqual(
+ channel.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent,
+ "General",
+ "should have name value"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commands`).length,
+ 1,
+ "should have commands"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_command`).length,
+ 2,
+ "should have 2 commands"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commandSettings`).length,
+ 1,
+ "should have 'settings' command"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commandLeave`).length,
+ 1,
+ "should have 'leave' command"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_counter`).length,
+ 0,
+ "should have a counter when equals 0 (default value)"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).click()
+ );
+ channel = document.querySelector(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`);
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator.o-item-active`).length,
+ 1,
+ "channel should become active"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_ThreadView_composer`).length,
+ 1,
+ "should have composer section inside thread content (can post message in channel)"
+ );
+});
+
+QUnit.test('sidebar: channel rendering with needaction counter', async function (assert) {
+ assert.expect(5);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id that will be used to link message
+ this.data['mail.channel'].records.push({ id: 20 });
+ // expected needaction message
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [20], // link message to channel
+ id: 100, // random unique id, useful to link notification
+ });
+ // expected needaction notification
+ this.data['mail.notification'].records.push({
+ mail_message_id: 100, // id of related message
+ res_partner_id: this.data.currentPartnerId, // must be for current partner
+ });
+ await this.start();
+ const channel = document.querySelector(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`);
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_counter`).length,
+ 1,
+ "should have a counter when different from 0"
+ );
+ assert.strictEqual(
+ channel.querySelector(`:scope .o_DiscussSidebarItem_counter`).textContent,
+ "1",
+ "should have counter value"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_command`).length,
+ 1,
+ "should have single command"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commandSettings`).length,
+ 1,
+ "should have 'settings' command"
+ );
+ assert.strictEqual(
+ channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commandLeave`).length,
+ 0,
+ "should not have 'leave' command"
+ );
+});
+
+QUnit.test('sidebar: mailing channel', async function (assert) {
+ assert.expect(1);
+
+ // channel that is expected to be in the sidebar, with proper mass_mailing value
+ this.data['mail.channel'].records.push({ mass_mailing: true });
+ await this.start();
+ assert.containsOnce(
+ document.querySelector(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`),
+ '.fa.fa-envelope-o',
+ "should have an icon to indicate that the channel is a mailing channel"
+ );
+});
+
+QUnit.test('sidebar: public/private channel rendering', async function (assert) {
+ assert.expect(5);
+
+ // channels that are expected to be found in the sidebar (one public, one private)
+ // with random unique id and name that will be referenced in the test
+ this.data['mail.channel'].records.push(
+ { id: 100, name: "channel1", public: 'public', },
+ { id: 101, name: "channel2", public: 'private' }
+ );
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length,
+ 2,
+ "should have 2 channel items"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 100,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).length,
+ 1,
+ "should have channel1 (Id 100)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 101,
+ model: 'mail.channel'
+ }).localId
+ }"]
+ `).length,
+ 1,
+ "should have channel2 (Id 101)"
+ );
+ const channel1 = document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 100,
+ model: 'mail.channel'
+ }).localId
+ }"]
+ `);
+ const channel2 = document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 101,
+ model: 'mail.channel'
+ }).localId
+ }"]
+ `);
+ assert.strictEqual(
+ channel1.querySelectorAll(`:scope .o_ThreadIcon_channelPublic`).length,
+ 1,
+ "channel1 (public) has hashtag icon"
+ );
+ assert.strictEqual(
+ channel2.querySelectorAll(`:scope .o_ThreadIcon_channelPrivate`).length,
+ 1,
+ "channel2 (private) has lock icon"
+ );
+});
+
+QUnit.test('sidebar: basic chat rendering', async function (assert) {
+ assert.expect(11);
+
+ // expected correspondent, with a random unique id that will be used to link
+ // partner to chat and a random name that will be asserted in the test
+ this.data['res.partner'].records.push({ id: 17, name: "Demo" });
+ // chat expected to be found in the sidebar
+ this.data['mail.channel'].records.push({
+ channel_type: 'chat', // testing a chat is the goal of the test
+ id: 10, // random unique id, will be referenced in the test
+ members: [this.data.currentPartnerId, 17], // expected partners
+ public: 'private', // expected value for testing a chat
+ });
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`).length,
+ 1,
+ "should have one chat item"
+ );
+ const chat = document.querySelector(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`);
+ assert.strictEqual(
+ chat.dataset.threadLocalId,
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel'
+ }).localId,
+ "should have chat with Id 10"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator`).length,
+ 1,
+ "should have active indicator"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_ThreadIcon`).length,
+ 1,
+ "should have an icon"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_name`).length,
+ 1,
+ "should have a name"
+ );
+ assert.strictEqual(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent,
+ "Demo",
+ "should have correspondent name as name"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commands`).length,
+ 1,
+ "should have commands"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_command`).length,
+ 2,
+ "should have 2 commands"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commandRename`).length,
+ 1,
+ "should have 'rename' command"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commandUnpin`).length,
+ 1,
+ "should have 'unpin' command"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_counter`).length,
+ 0,
+ "should have a counter when equals 0 (default value)"
+ );
+});
+
+QUnit.test('sidebar: chat rendering with unread counter', async function (assert) {
+ assert.expect(5);
+
+ // chat expected to be found in the sidebar
+ this.data['mail.channel'].records.push({
+ channel_type: 'chat', // testing a chat is the goal of the test
+ id: 10, // random unique id, will be referenced in the test
+ message_unread_counter: 100,
+ public: 'private', // expected value for testing a chat
+ });
+ await this.start();
+ const chat = document.querySelector(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`);
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_counter`).length,
+ 1,
+ "should have a counter when different from 0"
+ );
+ assert.strictEqual(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_counter`).textContent,
+ "100",
+ "should have counter value"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_command`).length,
+ 1,
+ "should have single command"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commandRename`).length,
+ 1,
+ "should have 'rename' command"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commandUnpin`).length,
+ 0,
+ "should not have 'unpin' command"
+ );
+});
+
+QUnit.test('sidebar: chat im_status rendering', async function (assert) {
+ assert.expect(7);
+
+ // expected correspondent, with a random unique id that will be used to link
+ // partner to chat, and various im_status values to assert
+ this.data['res.partner'].records.push(
+ { id: 101, im_status: 'offline', name: "Partner1" },
+ { id: 102, im_status: 'online', name: "Partner2" },
+ { id: 103, im_status: 'away', name: "Partner3" }
+ );
+ // chats expected to be found in the sidebar
+ this.data['mail.channel'].records.push(
+ {
+ channel_type: 'chat', // testing a chat is the goal of the test
+ id: 11, // random unique id, will be referenced in the test
+ members: [this.data.currentPartnerId, 101], // expected partners
+ public: 'private', // expected value for testing a chat
+ },
+ {
+ channel_type: 'chat', // testing a chat is the goal of the test
+ id: 12, // random unique id, will be referenced in the test
+ members: [this.data.currentPartnerId, 102], // expected partners
+ public: 'private', // expected value for testing a chat
+ },
+ {
+ channel_type: 'chat', // testing a chat is the goal of the test
+ id: 13, // random unique id, will be referenced in the test
+ members: [this.data.currentPartnerId, 103], // expected partners
+ public: 'private', // expected value for testing a chat
+ }
+ );
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`).length,
+ 3,
+ "should have 3 chat items"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).length,
+ 1,
+ "should have Partner1 (Id 11)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 12,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).length,
+ 1,
+ "should have Partner2 (Id 12)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 13,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).length,
+ 1,
+ "should have Partner3 (Id 13)"
+ );
+ const chat1 = document.querySelector(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ const chat2 = document.querySelector(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 12,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ const chat3 = document.querySelector(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 13,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ assert.strictEqual(
+ chat1.querySelectorAll(`:scope .o_ThreadIcon_offline`).length,
+ 1,
+ "chat1 should have offline icon"
+ );
+ assert.strictEqual(
+ chat2.querySelectorAll(`:scope .o_ThreadIcon_online`).length,
+ 1,
+ "chat2 should have online icon"
+ );
+ assert.strictEqual(
+ chat3.querySelectorAll(`:scope .o_ThreadIcon_away`).length,
+ 1,
+ "chat3 should have away icon"
+ );
+});
+
+QUnit.test('sidebar: chat custom name', async function (assert) {
+ assert.expect(1);
+
+ // expected correspondent, with a random unique id that will be used to link
+ // partner to chat, and a random name not used in the scope of this test but set for consistency
+ this.data['res.partner'].records.push({ id: 101, name: "Marc Demo" });
+ // chat expected to be found in the sidebar
+ this.data['mail.channel'].records.push({
+ channel_type: 'chat', // testing a chat is the goal of the test
+ custom_channel_name: "Marc", // testing a custom name is the goal of the test
+ members: [this.data.currentPartnerId, 101], // expected partners
+ public: 'private', // expected value for testing a chat
+ });
+ await this.start();
+ const chat = document.querySelector(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`);
+ assert.strictEqual(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent,
+ "Marc",
+ "chat should have custom name as name"
+ );
+});
+
+QUnit.test('sidebar: rename chat', async function (assert) {
+ assert.expect(8);
+
+ // expected correspondent, with a random unique id that will be used to link
+ // partner to chat, and a random name not used in the scope of this test but set for consistency
+ this.data['res.partner'].records.push({ id: 101, name: "Marc Demo" });
+ // chat expected to be found in the sidebar
+ this.data['mail.channel'].records.push({
+ channel_type: 'chat', // testing a chat is the goal of the test
+ custom_channel_name: "Marc", // testing a custom name is the goal of the test
+ members: [this.data.currentPartnerId, 101], // expected partners
+ public: 'private', // expected value for testing a chat
+ });
+ await this.start();
+ const chat = document.querySelector(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`);
+ assert.strictEqual(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent,
+ "Marc",
+ "chat should have custom name as name"
+ );
+ assert.notOk(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_name`).classList.contains('o-editable'),
+ "chat name should not be editable"
+ );
+
+ await afterNextRender(() =>
+ chat.querySelector(`:scope .o_DiscussSidebarItem_commandRename`).click()
+ );
+ assert.ok(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_name`).classList.contains('o-editable'),
+ "chat should have editable name"
+ );
+ assert.strictEqual(
+ chat.querySelectorAll(`:scope .o_DiscussSidebarItem_nameInput`).length,
+ 1,
+ "chat should have editable name input"
+ );
+ assert.strictEqual(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_nameInput`).value,
+ "Marc",
+ "editable name input should have custom chat name as value by default"
+ );
+ assert.strictEqual(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_nameInput`).placeholder,
+ "Marc Demo",
+ "editable name input should have partner name as placeholder"
+ );
+
+ await afterNextRender(() => {
+ chat.querySelector(`:scope .o_DiscussSidebarItem_nameInput`).value = "Demo";
+ const kevt = new window.KeyboardEvent('keydown', { key: "Enter" });
+ chat.querySelector(`:scope .o_DiscussSidebarItem_nameInput`).dispatchEvent(kevt);
+ });
+ assert.notOk(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_name`).classList.contains('o-editable'),
+ "chat should no longer show editable name"
+ );
+ assert.strictEqual(
+ chat.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent,
+ "Demo",
+ "chat should have renamed name as name"
+ );
+});
+
+QUnit.test('default thread rendering', async function (assert) {
+ assert.expect(16);
+
+ // channel expected to be found in the sidebar,
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `).length,
+ 1,
+ "should have inbox mailbox in the sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ `).length,
+ 1,
+ "should have starred mailbox in the sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).length,
+ 1,
+ "should have history mailbox in the sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).length,
+ 1,
+ "should have channel 20 in the sidebar"
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `).classList.contains('o-active'),
+ "inbox mailbox should be active thread"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty
+ `).length,
+ 1,
+ "should have empty thread in inbox"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty
+ `).textContent.trim(),
+ "Congratulations, your inbox is empty New messages appear here."
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ `).click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ `).classList.contains('o-active'),
+ "starred mailbox should be active thread"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty
+ `).length,
+ 1,
+ "should have empty thread in starred"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty
+ `).textContent.trim(),
+ "No starred messages You can mark any message as 'starred', and it shows up in this mailbox."
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).classList.contains('o-active'),
+ "history mailbox should be active thread"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty
+ `).length,
+ 1,
+ "should have empty thread in starred"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_Discuss_thread .o_MessageList_empty`).textContent.trim(),
+ "No history messages Messages marked as read will appear in the history."
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).classList.contains('o-active'),
+ "channel 20 should be active thread"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty
+ `).length,
+ 1,
+ "should have empty thread in starred"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty
+ `).textContent.trim(),
+ "There are no messages in this conversation."
+ );
+});
+
+QUnit.test('initially load messages from inbox', async function (assert) {
+ assert.expect(4);
+
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ assert.step('message_fetch');
+ assert.strictEqual(
+ args.kwargs.limit,
+ 30,
+ "should fetch up to 30 messages"
+ );
+ assert.deepEqual(
+ args.kwargs.domain,
+ [["needaction", "=", true]],
+ "should fetch needaction messages"
+ );
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.verifySteps(['message_fetch']);
+});
+
+QUnit.test('default select thread in discuss params', async function (assert) {
+ assert.expect(1);
+
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.box_starred',
+ },
+ }
+ });
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "starred mailbox should become active"
+ );
+});
+
+QUnit.test('auto-select thread in discuss context', async function (assert) {
+ assert.expect(1);
+
+ await this.start({
+ discuss: {
+ context: {
+ active_id: 'mail.box_starred',
+ },
+ },
+ });
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "starred mailbox should become active"
+ );
+});
+
+QUnit.test('load single message from channel initially', async function (assert) {
+ assert.expect(7);
+
+ // channel expected to be rendered, with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ model: 'mail.channel',
+ res_id: 20,
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ assert.strictEqual(
+ args.kwargs.limit,
+ 30,
+ "should fetch up to 30 messages"
+ );
+ assert.deepEqual(
+ args.kwargs.domain,
+ [["channel_ids", "in", [20]]],
+ "should fetch messages from channel"
+ );
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_ThreadView_messageList`).length,
+ 1,
+ "should have list of messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_MessageList_separatorDate`).length,
+ 1,
+ "should have a single date separator" // to check: may be client timezone dependent
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_Discuss_thread .o_MessageList_separatorLabelDate`).textContent,
+ "April 20, 2019",
+ "should display date day of messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_MessageList_message`).length,
+ 1,
+ "should have a single message"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `).length,
+ 1,
+ "should have message with Id 100"
+ );
+});
+
+QUnit.test('open channel from active_id as channel id', async function (assert) {
+ assert.expect(1);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start({
+ discuss: {
+ context: {
+ active_id: 20,
+ },
+ }
+ });
+ assert.containsOnce(
+ document.body,
+ `
+ .o_Discuss_thread[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({ id: 20, model: 'mail.channel' }).localId
+ }"]
+ `,
+ "should have channel with ID 20 open in Discuss when providing active_id 20"
+ );
+});
+
+QUnit.test('basic rendering of message', async function (assert) {
+ // AKU TODO: should be in message-only tests
+ assert.expect(13);
+
+ // channel expected to be rendered, with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ // partner to be set as author, with a random unique id that will be used to
+ // link message and a random name that will be asserted in the test
+ this.data['res.partner'].records.push({ id: 11, name: "Demo" });
+ this.data['mail.message'].records.push({
+ author_id: 11,
+ body: "<p>body</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ model: 'mail.channel',
+ res_id: 20,
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ const message = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadView_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `);
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_sidebar`).length,
+ 1,
+ "should have message sidebar of message"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_authorAvatar`).length,
+ 1,
+ "should have author avatar in sidebar of message"
+ );
+ assert.strictEqual(
+ message.querySelector(`:scope .o_Message_authorAvatar`).dataset.src,
+ "/web/image/res.partner/11/image_128",
+ "should have url of message in author avatar sidebar"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_core`).length,
+ 1,
+ "should have core part of message"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_header`).length,
+ 1,
+ "should have header in core part of message"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_authorName`).length,
+ 1,
+ "should have author name in header of message"
+ );
+ assert.strictEqual(
+ message.querySelector(`:scope .o_Message_authorName`).textContent,
+ "Demo",
+ "should have textually author name in header of message"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_header .o_Message_date`).length,
+ 1,
+ "should have date in header of message"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_header .o_Message_commands`).length,
+ 1,
+ "should have commands in header of message"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_header .o_Message_command`).length,
+ 1,
+ "should have a single command in header of message"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_commandStar`).length,
+ 1,
+ "should have command to star message"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_content`).length,
+ 1,
+ "should have content in core part of message"
+ );
+ assert.strictEqual(
+ message.querySelector(`:scope .o_Message_content`).textContent.trim(),
+ "body",
+ "should have body of message in content part of message"
+ );
+});
+
+QUnit.test('basic rendering of squashed message', async function (assert) {
+ // messages are squashed when "close", e.g. less than 1 minute has elapsed
+ // from messages of same author and same thread. Note that this should
+ // be working in non-mailboxes
+ // AKU TODO: should be message and/or message list-only tests
+ assert.expect(12);
+
+ // channel expected to be rendered, with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ // partner to be set as author, with a random unique id that will be used to link message
+ this.data['res.partner'].records.push({ id: 11 });
+ this.data['mail.message'].records.push(
+ {
+ author_id: 11, // must be same author as other message
+ body: "<p>body1</p>", // random body, set for consistency
+ channel_ids: [20], // to link message to channel
+ date: "2019-04-20 10:00:00", // date must be within 1 min from other message
+ id: 100, // random unique id, will be referenced in the test
+ message_type: 'comment', // must be a squash-able type-
+ model: 'mail.channel', // to link message to channel
+ res_id: 20, // id of related channel
+ },
+ {
+ author_id: 11, // must be same author as other message
+ body: "<p>body2</p>", // random body, will be asserted in the test
+ channel_ids: [20], // to link message to channel
+ date: "2019-04-20 10:00:30", // date must be within 1 min from other message
+ id: 101, // random unique id, will be referenced in the test
+ message_type: 'comment', // must be a squash-able type
+ model: 'mail.channel', // to link message to channel
+ res_id: 20, // id of related channel
+ }
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 2,
+ "should have 2 messages"
+ );
+ const message1 = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadView_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `);
+ const message2 = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadView_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ `);
+ assert.notOk(
+ message1.classList.contains('o-squashed'),
+ "message 1 should not be squashed"
+ );
+ assert.notOk(
+ message1.querySelector(`:scope .o_Message_sidebar`).classList.contains('o-message-squashed'),
+ "message 1 should not have squashed sidebar"
+ );
+ assert.ok(
+ message2.classList.contains('o-squashed'),
+ "message 2 should be squashed"
+ );
+ assert.ok(
+ message2.querySelector(`:scope .o_Message_sidebar`).classList.contains('o-message-squashed'),
+ "message 2 should have squashed sidebar"
+ );
+ assert.strictEqual(
+ message2.querySelectorAll(`:scope .o_Message_sidebar .o_Message_date`).length,
+ 1,
+ "message 2 should have date in sidebar"
+ );
+ assert.strictEqual(
+ message2.querySelectorAll(`:scope .o_Message_sidebar .o_Message_commands`).length,
+ 1,
+ "message 2 should have some commands in sidebar"
+ );
+ assert.strictEqual(
+ message2.querySelectorAll(`:scope .o_Message_sidebar .o_Message_commandStar`).length,
+ 1,
+ "message 2 should have star command in sidebar"
+ );
+ assert.strictEqual(
+ message2.querySelectorAll(`:scope .o_Message_core`).length,
+ 1,
+ "message 2 should have core part"
+ );
+ assert.strictEqual(
+ message2.querySelectorAll(`:scope .o_Message_header`).length,
+ 0,
+ "message 2 should have a header in core part"
+ );
+ assert.strictEqual(
+ message2.querySelectorAll(`:scope .o_Message_content`).length,
+ 1,
+ "message 2 should have some content in core part"
+ );
+ assert.strictEqual(
+ message2.querySelector(`:scope .o_Message_content`).textContent.trim(),
+ "body2",
+ "message 2 should have body in content part"
+ );
+});
+
+QUnit.test('inbox messages are never squashed', async function (assert) {
+ assert.expect(3);
+
+ // partner to be set as author, with a random unique id that will be used to link message
+ this.data['res.partner'].records.push({ id: 11 });
+ this.data['mail.message'].records.push(
+ {
+ author_id: 11, // must be same author as other message
+ body: "<p>body1</p>", // random body, set for consistency
+ channel_ids: [20], // to link message to channel
+ date: "2019-04-20 10:00:00", // date must be within 1 min from other message
+ id: 100, // random unique id, will be referenced in the test
+ message_type: 'comment', // must be a squash-able type-
+ model: 'mail.channel', // to link message to channel
+ needaction: true, // necessary for message_fetch domain
+ needaction_partner_ids: [this.data.currentPartnerId], // for consistency
+ res_id: 20, // id of related channel
+ },
+ {
+ author_id: 11, // must be same author as other message
+ body: "<p>body2</p>", // random body, will be asserted in the test
+ channel_ids: [20], // to link message to channel
+ date: "2019-04-20 10:00:30", // date must be within 1 min from other message
+ id: 101, // random unique id, will be referenced in the test
+ message_type: 'comment', // must be a squash-able type
+ model: 'mail.channel', // to link message to channel
+ needaction: true, // necessary for message_fetch domain
+ needaction_partner_ids: [this.data.currentPartnerId], // for consistency
+ res_id: 20, // id of related channel
+ }
+ );
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 2,
+ "should have 2 messages"
+ );
+ const message1 = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadView_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `);
+ const message2 = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadView_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ `);
+ assert.notOk(
+ message1.classList.contains('o-squashed'),
+ "message 1 should not be squashed"
+ );
+ assert.notOk(
+ message2.classList.contains('o-squashed'),
+ "message 2 should not be squashed"
+ );
+});
+
+QUnit.test('load all messages from channel initially, less than fetch limit (29 < 30)', async function (assert) {
+ // AKU TODO: thread specific test
+ assert.expect(5);
+
+ // channel expected to be rendered, with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ // partner to be set as author, with a random unique id that will be used to link message
+ this.data['res.partner'].records.push({ id: 11 });
+ for (let i = 28; i >= 0; i--) {
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ model: 'mail.channel',
+ res_id: 20,
+ });
+ }
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ assert.strictEqual(args.kwargs.limit, 30, "should fetch up to 30 messages");
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_separatorDate
+ `).length,
+ 1,
+ "should have a single date separator" // to check: may be client timezone dependent
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_separatorLabelDate
+ `).textContent,
+ "April 20, 2019",
+ "should display date day of messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 29,
+ "should have 29 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_loadMore
+ `).length,
+ 0,
+ "should not have load more link"
+ );
+});
+
+QUnit.test('load more messages from channel', async function (assert) {
+ // AKU: thread specific test
+ assert.expect(6);
+
+ // channel expected to be rendered, with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ // partner to be set as author, with a random unique id that will be used to link message
+ this.data['res.partner'].records.push({ id: 11 });
+ for (let i = 0; i < 40; i++) {
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ model: 'mail.channel',
+ res_id: 20,
+ });
+ }
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_separatorDate
+ `).length,
+ 1,
+ "should have a single date separator" // to check: may be client timezone dependent
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_separatorLabelDate
+ `).textContent,
+ "April 20, 2019",
+ "should display date day of messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 30,
+ "should have 30 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_loadMore
+ `).length,
+ 1,
+ "should have load more link"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_loadMore
+ `).click()
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 40,
+ "should have 40 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_loadMore
+ `).length,
+ 0,
+ "should not longer have load more link (all messages loaded)"
+ );
+});
+
+QUnit.test('auto-scroll to bottom of thread', async function (assert) {
+ // AKU TODO: thread specific test
+ assert.expect(2);
+
+ // channel expected to be rendered, with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ for (let i = 1; i <= 25; i++) {
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [20],
+ model: 'mail.channel',
+ res_id: 20,
+ });
+ }
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ waitUntilEvent: {
+ eventName: 'o-component-message-list-scrolled',
+ message: "should wait until channel 20 scrolled to its last message initially",
+ predicate: ({ scrollTop, thread }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ thread &&
+ thread.model === 'mail.channel' &&
+ thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 25,
+ "should have 25 messages"
+ );
+ const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
+ assert.strictEqual(
+ messageList.scrollTop,
+ messageList.scrollHeight - messageList.clientHeight,
+ "should have scrolled to bottom of thread"
+ );
+});
+
+QUnit.test('load more messages from channel (auto-load on scroll)', async function (assert) {
+ // AKU TODO: thread specific test
+ assert.expect(3);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ for (let i = 0; i < 40; i++) {
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [20],
+ model: 'mail.channel',
+ res_id: 20,
+ });
+ }
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ waitUntilEvent: {
+ eventName: 'o-component-message-list-scrolled',
+ message: "should wait until channel 20 scrolled to its last message initially",
+ predicate: ({ scrollTop, thread }) => {
+ const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
+ return (
+ thread &&
+ thread.model === 'mail.channel' &&
+ thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 30,
+ "should have 30 messages"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => document.querySelector('.o_ThreadView_messageList').scrollTop = 0,
+ message: "should wait until channel 20 loaded more messages after scrolling to top",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'more-messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20
+ );
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 40,
+ "should have 40 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Dsiscuss_thread .o_ThreadView_messageList .o_MessageList_loadMore
+ `).length,
+ 0,
+ "should not longer have load more link (all messages loaded)"
+ );
+});
+
+QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
+ // this test requires several messages so that the last message is not
+ // visible. This is necessary in order to display 'new messages' and not
+ // remove from DOM right away from seeing last message.
+ // AKU TODO: thread specific test
+ assert.expect(6);
+
+ // Needed partner & user to allow simulation of message reception
+ this.data['res.partner'].records.push({
+ id: 11,
+ name: "Foreigner partner",
+ });
+ this.data['res.users'].records.push({
+ id: 42,
+ name: "Foreigner user",
+ partner_id: 11,
+ });
+ // channel expected to be rendered, with a random unique id that will be
+ // referenced in the test and the seen_message_id value set to last message
+ this.data['mail.channel'].records.push({
+ id: 20,
+ seen_message_id: 125,
+ uuid: 'randomuuid',
+ });
+ for (let i = 1; i <= 25; i++) {
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [20],
+ id: 100 + i, // for setting proper value for seen_message_id
+ model: 'mail.channel',
+ res_id: 20,
+ });
+ }
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ waitUntilEvent: {
+ eventName: 'o-component-message-list-scrolled',
+ message: "should wait until channel 20 scrolled to its last message initially",
+ predicate: ({ scrollTop, thread }) => {
+ const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
+ return (
+ thread &&
+ thread.model === 'mail.channel' &&
+ thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ },
+ });
+ assert.containsN(
+ document.body,
+ '.o_MessageList_message',
+ 25,
+ "should have 25 messages"
+ );
+ assert.containsNone(
+ document.body,
+ '.o_MessageList_separatorNewMessages',
+ "should not display 'new messages' separator"
+ );
+ // scroll to top
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => {
+ document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0;
+ },
+ message: "should wait until channel scrolled to top",
+ predicate: ({ scrollTop, thread }) => {
+ return (
+ thread &&
+ thread.model === 'mail.channel' &&
+ thread.id === 20 &&
+ scrollTop === 0
+ );
+ },
+ });
+ // composer is focused by default, we remove that focus
+ document.querySelector('.o_ComposerTextInput_textarea').blur();
+ // simulate receiving a message
+ await afterNextRender(async () => this.env.services.rpc({
+ route: '/mail/chat_post',
+ params: {
+ context: {
+ mockedUserId: 42,
+ },
+ message_content: "hu",
+ uuid: 'randomuuid',
+ },
+ }));
+
+ assert.containsN(
+ document.body,
+ '.o_MessageList_message',
+ 26,
+ "should have 26 messages"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_MessageList_separatorNewMessages',
+ "should display 'new messages' separator"
+ );
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => {
+ const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
+ messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight;
+ },
+ message: "should wait until channel scrolled to bottom",
+ predicate: ({ scrollTop, thread }) => {
+ const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
+ return (
+ thread &&
+ thread.model === 'mail.channel' &&
+ thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_MessageList_separatorNewMessages',
+ "should still display 'new messages' separator as composer is not focused"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerTextInput_textarea').focus()
+ );
+ assert.containsNone(
+ document.body,
+ '.o_MessageList_separatorNewMessages',
+ "should no longer display 'new messages' separator (message seen)"
+ );
+});
+
+QUnit.test('restore thread scroll position', async function (assert) {
+ assert.expect(6);
+ // channels expected to be rendered, with random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push(
+ {
+ id: 11,
+ },
+ {
+ id: 12,
+ },
+ );
+ for (let i = 1; i <= 25; i++) {
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [11],
+ model: 'mail.channel',
+ res_id: 11,
+ });
+ }
+ for (let i = 1; i <= 24; i++) {
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [12],
+ model: 'mail.channel',
+ res_id: 12,
+ });
+ }
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_11',
+ },
+ },
+ waitUntilEvent: {
+ eventName: 'o-component-message-list-scrolled',
+ message: "should wait until channel 11 scrolled to its last message",
+ predicate: ({ thread }) => {
+ return thread && thread.model === 'mail.channel' && thread.id === 11;
+ },
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 25,
+ "should have 25 messages in channel 11"
+ );
+ const initialMessageList = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadView_messageList
+ `);
+ assert.strictEqual(
+ initialMessageList.scrollTop,
+ initialMessageList.scrollHeight - initialMessageList.clientHeight,
+ "should have scrolled to bottom of channel 11 initially"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0,
+ message: "should wait until channel 11 changed its scroll position to top",
+ predicate: ({ thread }) => {
+ return thread && thread.model === 'mail.channel' && thread.id === 11;
+ },
+ });
+ assert.strictEqual(
+ document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop,
+ 0,
+ "should have scrolled to top of channel 11",
+ );
+
+ // Ensure scrollIntoView of channel 12 has enough time to complete before
+ // going back to channel 11. Await is needed to prevent the scrollIntoView
+ // initially planned for channel 12 to actually apply on channel 11.
+ // task-2333535
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => {
+ // select channel 12
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 12,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click();
+ },
+ message: "should wait until channel 12 scrolled to its last message",
+ predicate: ({ scrollTop, thread }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ thread &&
+ thread.model === 'mail.channel' &&
+ thread.id === 12 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 24,
+ "should have 24 messages in channel 12"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => {
+ // select channel 11
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click();
+ },
+ message: "should wait until channel 11 restored its scroll position",
+ predicate: ({ scrollTop, thread }) => {
+ return (
+ thread &&
+ thread.model === 'mail.channel' &&
+ thread.id === 11 &&
+ scrollTop === 0
+ );
+ },
+ });
+ assert.strictEqual(
+ document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop,
+ 0,
+ "should have recovered scroll position of channel 11 (scroll to top)"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => {
+ // select channel 12
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 12,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click();
+ },
+ message: "should wait until channel 12 recovered its scroll position (to bottom)",
+ predicate: ({ scrollTop, thread }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ thread &&
+ thread.model === 'mail.channel' &&
+ thread.id === 12 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ });
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ assert.strictEqual(
+ messageList.scrollTop,
+ messageList.scrollHeight - messageList.clientHeight,
+ "should have recovered scroll position of channel 12 (scroll to bottom)"
+ );
+});
+
+QUnit.test('message origin redirect to channel', async function (assert) {
+ assert.expect(15);
+
+ // channels expected to be rendered, with random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 11 }, { id: 12 });
+ this.data['mail.message'].records.push(
+ {
+ body: "not empty",
+ channel_ids: [11, 12],
+ id: 100,
+ model: 'mail.channel',
+ record_name: "channel11",
+ res_id: 11,
+ },
+ {
+ body: "not empty",
+ channel_ids: [11, 12],
+ id: 101,
+ model: 'mail.channel',
+ record_name: "channel12",
+ res_id: 12,
+ }
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_11',
+ },
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Discuss_thread .o_Message').length,
+ 2,
+ "should have 2 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `).length,
+ 1,
+ "should have message1 (Id 100)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ `).length,
+ 1,
+ "should have message2 (Id 101)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ .o_Message_originThread
+ `).length,
+ 0,
+ "message1 should not have origin part in channel11 (same origin as channel)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ .o_Message_originThread
+ `).length,
+ 1,
+ "message2 should have origin part (origin is channel12 !== channel11)"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ .o_Message_originThread
+ `).textContent.trim(),
+ "(from #channel12)",
+ "message2 should display name of origin channel"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ .o_Message_originThreadLink
+ `).length,
+ 1,
+ "message2 should have link to redirect to origin"
+ );
+
+ // click on origin link of message2 (= channel12)
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ .o_Message_originThreadLink
+ `).click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 12,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "channel12 should be active channel on redirect from discuss app"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_Message`).length,
+ 2,
+ "should have 2 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `).length,
+ 1,
+ "should have message1 (Id 100)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ `).length,
+ 1,
+ "should have message2 (Id 101)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ .o_Message_originThread
+ `).length,
+ 1,
+ "message1 should have origin thread part (= channel11 !== channel12)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ .o_Message_originThread
+ `).length,
+ 0,
+ "message2 should not have origin thread part in channel12 (same as current channel)"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ .o_Message_originThread
+ `).textContent.trim(),
+ "(from #channel11)",
+ "message1 should display name of origin channel"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ .o_Message_originThreadLink
+ `).length,
+ 1,
+ "message1 should have link to redirect to origin channel"
+ );
+});
+
+QUnit.test('redirect to author (open chat)', async function (assert) {
+ assert.expect(7);
+
+ // expected correspondent, with a random unique id that will be used to link
+ // partner to chat and a random name that will be asserted in the test
+ this.data['res.partner'].records.push({ id: 7, name: "Demo" });
+ this.data['res.users'].records.push({ partner_id: 7 });
+ this.data['mail.channel'].records.push(
+ // channel expected to be found in the sidebar
+ {
+ id: 1, // random unique id, will be referenced in the test
+ name: "General", // random name, will be asserted in the test
+ },
+ // chat expected to be found in the sidebar
+ {
+ channel_type: 'chat', // testing a chat is the goal of the test
+ id: 10, // random unique id, will be referenced in the test
+ members: [this.data.currentPartnerId, 7], // expected partners
+ public: 'private', // expected value for testing a chat
+ }
+ );
+ this.data['mail.message'].records.push(
+ {
+ author_id: 7,
+ body: "not empty",
+ channel_ids: [1],
+ id: 100,
+ model: 'mail.channel',
+ res_id: 1,
+ }
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_1',
+ },
+ },
+ });
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 1,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "channel 'General' should be active"
+ );
+ assert.notOk(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "Chat 'Demo' should not be active"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_Message`).length,
+ 1,
+ "should have 1 message"
+ );
+ const msg1 = document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `);
+ assert.strictEqual(
+ msg1.querySelectorAll(`:scope .o_Message_authorAvatar`).length,
+ 1,
+ "message1 should have author image"
+ );
+ assert.ok(
+ msg1.querySelector(`:scope .o_Message_authorAvatar`).classList.contains('o_redirect'),
+ "message1 should have redirect to author"
+ );
+
+ await afterNextRender(() =>
+ msg1.querySelector(`:scope .o_Message_authorAvatar`).click()
+ );
+ assert.notOk(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 1,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "channel 'General' should become inactive after author redirection"
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "chat 'Demo' should become active after author redirection"
+ );
+});
+
+QUnit.test('sidebar quick search', async function (assert) {
+ // feature enables at 20 or more channels
+ assert.expect(6);
+
+ for (let id = 1; id <= 20; id++) {
+ this.data['mail.channel'].records.push({ id, name: `channel${id}` });
+ }
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length,
+ 20,
+ "should have 20 channel items"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_sidebar input.o_DiscussSidebar_quickSearch`).length,
+ 1,
+ "should have quick search in sidebar"
+ );
+
+ const quickSearch = document.querySelector(`
+ .o_Discuss_sidebar input.o_DiscussSidebar_quickSearch
+ `);
+ await afterNextRender(() => {
+ quickSearch.value = "1";
+ const kevt1 = new window.KeyboardEvent('input');
+ quickSearch.dispatchEvent(kevt1);
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length,
+ 11,
+ "should have filtered to 11 channel items"
+ );
+
+ await afterNextRender(() => {
+ quickSearch.value = "12";
+ const kevt2 = new window.KeyboardEvent('input');
+ quickSearch.dispatchEvent(kevt2);
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length,
+ 1,
+ "should have filtered to a single channel item"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel .o_DiscussSidebar_item
+ `).dataset.threadLocalId,
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 12,
+ model: 'mail.channel',
+ }).localId,
+ "should have filtered to a single channel item with Id 12"
+ );
+
+ await afterNextRender(() => {
+ quickSearch.value = "123";
+ const kevt3 = new window.KeyboardEvent('input');
+ quickSearch.dispatchEvent(kevt3);
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length,
+ 0,
+ "should have filtered to no channel item"
+ );
+});
+
+QUnit.test('basic control panel rendering', async function (assert) {
+ assert.expect(8);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id and name that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20, name: "General" });
+ await this.start();
+ assert.strictEqual(
+ document.querySelector(`
+ .o_widget_Discuss .o_control_panel .breadcrumb
+ `).textContent,
+ "Inbox",
+ "display inbox in the breadcrumb"
+ );
+ const markAllReadButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonMarkAllRead`);
+ assert.isVisible(
+ markAllReadButton,
+ "should have visible button 'Mark all read' in the control panel of inbox"
+ );
+ assert.ok(
+ markAllReadButton.disabled,
+ "should have disabled button 'Mark all read' in the control panel of inbox (no messages)"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ `).click()
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_widget_Discuss .o_control_panel .breadcrumb
+ `).textContent,
+ "Starred",
+ "display starred in the breadcrumb"
+ );
+ const unstarAllButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonUnstarAll`);
+ assert.isVisible(
+ unstarAllButton,
+ "should have visible button 'Unstar all' in the control panel of starred"
+ );
+ assert.ok(
+ unstarAllButton.disabled,
+ "should have disabled button 'Unstar all' in the control panel of starred (no messages)"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click()
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_widget_Discuss .o_control_panel .breadcrumb
+ `).textContent,
+ "#General",
+ "display general in the breadcrumb"
+ );
+ const inviteButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonInvite`);
+ assert.isVisible(
+ inviteButton,
+ "should have visible button 'Invite' in the control panel of channel"
+ );
+});
+
+QUnit.test('inbox: mark all messages as read', async function (assert) {
+ assert.expect(8);
+
+ // channel expected to be found in the sidebar,
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ this.data['mail.message'].records.push(
+ // first expected message
+ {
+ body: "not empty",
+ channel_ids: [20], // link message to channel
+ id: 100, // random unique id, useful to link notification
+ model: 'mail.channel',
+ // needaction needs to be set here for message_fetch domain, because
+ // mocked models don't have computed fields
+ needaction: true,
+ res_id: 20,
+ },
+ // second expected message
+ {
+ body: "not empty",
+ channel_ids: [20], // link message to channel
+ id: 101, // random unique id, useful to link notification
+ model: 'mail.channel',
+ // needaction needs to be set here for message_fetch domain, because
+ // mocked models don't have computed fields
+ needaction: true,
+ res_id: 20,
+ }
+ );
+ this.data['mail.notification'].records.push(
+ // notification to have first message in inbox
+ {
+ mail_message_id: 100, // id of related message
+ res_partner_id: this.data.currentPartnerId, // must be for current partner
+ },
+ // notification to have second message in inbox
+ {
+ mail_message_id: 101, // id of related message
+ res_partner_id: this.data.currentPartnerId, // must be for current partner
+ }
+ );
+ await this.start();
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).textContent,
+ "2",
+ "inbox should have counter of 2"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).textContent,
+ "2",
+ "channel should have counter of 2"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss .o_Message`).length,
+ 2,
+ "should have 2 messages in inbox"
+ );
+ let markAllReadButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonMarkAllRead`);
+ assert.notOk(
+ markAllReadButton.disabled,
+ "should have enabled button 'Mark all read' in the control panel of inbox (has messages)"
+ );
+
+ await afterNextRender(() => markAllReadButton.click());
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).length,
+ 0,
+ "inbox should display no counter (= 0)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).length,
+ 0,
+ "channel should display no counter (= 0)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss .o_Message`).length,
+ 0,
+ "should have no message in inbox"
+ );
+ markAllReadButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonMarkAllRead`);
+ assert.ok(
+ markAllReadButton.disabled,
+ "should have disabled button 'Mark all read' in the control panel of inbox (no messages)"
+ );
+});
+
+QUnit.test('starred: unstar all', async function (assert) {
+ assert.expect(6);
+
+ // messages expected to be starred
+ this.data['mail.message'].records.push(
+ { body: "not empty", starred_partner_ids: [this.data.currentPartnerId] },
+ { body: "not empty", starred_partner_ids: [this.data.currentPartnerId] }
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.box_starred',
+ },
+ },
+ });
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).textContent,
+ "2",
+ "starred should have counter of 2"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss .o_Message`).length,
+ 2,
+ "should have 2 messages in starred"
+ );
+ let unstarAllButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonUnstarAll`);
+ assert.notOk(
+ unstarAllButton.disabled,
+ "should have enabled button 'Unstar all' in the control panel of starred (has messages)"
+ );
+
+ await afterNextRender(() => unstarAllButton.click());
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).length,
+ 0,
+ "starred should display no counter (= 0)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss .o_Message`).length,
+ 0,
+ "should have no message in starred"
+ );
+ unstarAllButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonUnstarAll`);
+ assert.ok(
+ unstarAllButton.disabled,
+ "should have disabled button 'Unstar all' in the control panel of starred (no messages)"
+ );
+});
+
+QUnit.test('toggle_star message', async function (assert) {
+ assert.expect(16);
+
+ // channel expected to be initially rendered
+ // with a random unique id, will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [20],
+ id: 100,
+ model: 'mail.channel',
+ res_id: 20,
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'toggle_message_starred') {
+ assert.step('rpc:toggle_message_starred');
+ assert.strictEqual(
+ args.args[0][0],
+ 100,
+ "should have message Id in args"
+ );
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).length,
+ 0,
+ "starred should display no counter (= 0)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss .o_Message`).length,
+ 1,
+ "should have 1 message in channel"
+ );
+ let message = document.querySelector(`.o_Discuss .o_Message`);
+ assert.notOk(
+ message.classList.contains('o-starred'),
+ "message should not be starred"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_commandStar`).length,
+ 1,
+ "message should have star command"
+ );
+
+ await afterNextRender(() => message.querySelector(`:scope .o_Message_commandStar`).click());
+ assert.verifySteps(['rpc:toggle_message_starred']);
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).textContent,
+ "1",
+ "starred should display a counter of 1"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss .o_Message`).length,
+ 1,
+ "should have kept 1 message in channel"
+ );
+ message = document.querySelector(`.o_Discuss .o_Message`);
+ assert.ok(
+ message.classList.contains('o-starred'),
+ "message should be starred"
+ );
+
+ await afterNextRender(() => message.querySelector(`:scope .o_Message_commandStar`).click());
+ assert.verifySteps(['rpc:toggle_message_starred']);
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.starred.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).length,
+ 0,
+ "starred should no longer display a counter (= 0)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss .o_Message`).length,
+ 1,
+ "should still have 1 message in channel"
+ );
+ message = document.querySelector(`.o_Discuss .o_Message`);
+ assert.notOk(
+ message.classList.contains('o-starred'),
+ "message should no longer be starred"
+ );
+});
+
+QUnit.test('composer state: text save and restore', async function (assert) {
+ assert.expect(2);
+
+ // channels expected to be found in the sidebar,
+ // with random unique id and name that will be referenced in the test
+ this.data['mail.channel'].records.push(
+ { id: 20, name: "General" },
+ { id: 21, name: "Special" }
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ // Write text in composer for #general
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "A message");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('input'));
+ });
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebarItem[data-thread-name="Special"]`).click()
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "An other message");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('input'));
+ });
+ // Switch back to #general
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebarItem[data-thread-name="General"]`).click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "A message",
+ "should restore the input text"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebarItem[data-thread-name="Special"]`).click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "An other message",
+ "should restore the input text"
+ );
+});
+
+QUnit.test('composer state: attachments save and restore', async function (assert) {
+ assert.expect(6);
+
+ // channels expected to be found in the sidebar
+ // with random unique id and name that will be referenced in the test
+ this.data['mail.channel'].records.push(
+ { id: 20, name: "General" },
+ { id: 21, name: "Special" }
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ const channels = document.querySelectorAll(`
+ .o_DiscussSidebar_groupChannel .o_DiscussSidebar_item
+ `);
+ // Add attachment in a message for #general
+ await afterNextRender(async () => {
+ const file = await createFile({
+ content: 'hello, world',
+ contentType: 'text/plain',
+ name: 'text.txt',
+ });
+ inputFiles(
+ document.querySelector('.o_FileUploader_input'),
+ [file]
+ );
+ });
+ // Switch to #special
+ await afterNextRender(() => channels[1].click());
+ // Add attachments in a message for #special
+ const files = [
+ await createFile({
+ content: 'hello2, world',
+ contentType: 'text/plain',
+ name: 'text2.txt',
+ }),
+ await createFile({
+ content: 'hello3, world',
+ contentType: 'text/plain',
+ name: 'text3.txt',
+ }),
+ await createFile({
+ content: 'hello4, world',
+ contentType: 'text/plain',
+ name: 'text4.txt',
+ }),
+ ];
+ await afterNextRender(() =>
+ inputFiles(
+ document.querySelector('.o_FileUploader_input'),
+ files
+ )
+ );
+ // Switch back to #general
+ await afterNextRender(() => channels[0].click());
+ // Check attachment is reloaded
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Composer .o_Attachment`).length,
+ 1,
+ "should have 1 attachment in the composer"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_Composer .o_Attachment`).dataset.attachmentLocalId,
+ this.env.models['mail.attachment'].findFromIdentifyingData({ id: 1 }).localId,
+ "should have correct 1st attachment in the composer"
+ );
+
+ // Switch back to #special
+ await afterNextRender(() => channels[1].click());
+ // Check attachments are reloaded
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Composer .o_Attachment`).length,
+ 3,
+ "should have 3 attachments in the composer"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Composer .o_Attachment`)[0].dataset.attachmentLocalId,
+ this.env.models['mail.attachment'].findFromIdentifyingData({ id: 2 }).localId,
+ "should have attachment with id 2 as 1st attachment"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Composer .o_Attachment`)[1].dataset.attachmentLocalId,
+ this.env.models['mail.attachment'].findFromIdentifyingData({ id: 3 }).localId,
+ "should have attachment with id 3 as 2nd attachment"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Composer .o_Attachment`)[2].dataset.attachmentLocalId,
+ this.env.models['mail.attachment'].findFromIdentifyingData({ id: 4 }).localId,
+ "should have attachment with id 4 as 3rd attachment"
+ );
+});
+
+QUnit.test('post a simple message', async function (assert) {
+ assert.expect(15);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ let postedMessageId;
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ const res = await this._super(...arguments);
+ if (args.method === 'message_post') {
+ assert.step('message_post');
+ assert.strictEqual(
+ args.args[0],
+ 20,
+ "should post message to channel Id 20"
+ );
+ assert.strictEqual(
+ args.kwargs.body,
+ "Test",
+ "should post with provided content in composer input"
+ );
+ assert.strictEqual(
+ args.kwargs.message_type,
+ "comment",
+ "should set message type as 'comment'"
+ );
+ assert.strictEqual(
+ args.kwargs.subtype_xmlid,
+ "mail.mt_comment",
+ "should set subtype_xmlid as 'comment'"
+ );
+ postedMessageId = res;
+ }
+ return res;
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_MessageList_empty`).length,
+ 1,
+ "should display thread with no message initially"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Message`).length,
+ 0,
+ "should display no message initially"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "should have empty content initially"
+ );
+
+ // insert some HTML in editable
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "Test");
+ });
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "Test",
+ "should have inserted text in editable"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Composer_buttonSend').click()
+ );
+ assert.verifySteps(['message_post']);
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "should have no content in composer input after posting message"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Message`).length,
+ 1,
+ "should display a message after posting message"
+ );
+ const message = document.querySelector(`.o_Message`);
+ assert.strictEqual(
+ message.dataset.messageLocalId,
+ this.env.models['mail.message'].findFromIdentifyingData({ id: postedMessageId }).localId,
+ "new message in thread should be linked to newly created message from message post"
+ );
+ assert.strictEqual(
+ message.querySelector(`:scope .o_Message_authorName`).textContent,
+ "Mitchell Admin",
+ "new message in thread should be from current partner name"
+ );
+ assert.strictEqual(
+ message.querySelector(`:scope .o_Message_content`).textContent,
+ "Test",
+ "new message in thread should have content typed from composer text input"
+ );
+});
+
+QUnit.test('post message on non-mailing channel with "Enter" keyboard shortcut', async function (assert) {
+ assert.expect(2);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20, mass_mailing: false });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should not have any message initially in channel"
+ );
+
+ // insert some HTML in editable
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "Test");
+ });
+ await afterNextRender(() => {
+ const kevt = new window.KeyboardEvent('keydown', { key: "Enter" });
+ 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 'Enter' in text input of composer"
+ );
+});
+
+QUnit.test('do not post message on non-mailing channel with "SHIFT-Enter" keyboard shortcut', async function (assert) {
+ // Note that test doesn't assert SHIFT-Enter makes a newline, because this
+ // default browser cannot be simulated with just dispatching
+ // programmatically crafted events...
+ assert.expect(2);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20, mass_mailing: true });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should not have any message initially in channel"
+ );
+
+ // insert some HTML in editable
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "Test");
+ });
+ const kevt = new window.KeyboardEvent('keydown', { key: "Enter", shiftKey: true });
+ document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(kevt);
+ await nextAnimationFrame();
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should still not have any message in channel after pressing 'Shift-Enter' in text input of composer"
+ );
+});
+
+QUnit.test('post message on mailing channel with "CTRL-Enter" keyboard shortcut', async function (assert) {
+ assert.expect(2);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20, mass_mailing: true });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should not have any message initially in channel"
+ );
+
+ // insert some HTML in editable
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "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 channel after posting message from pressing 'CTRL-Enter' in text input of composer"
+ );
+});
+
+QUnit.test('post message on mailing channel with "META-Enter" keyboard shortcut', async function (assert) {
+ assert.expect(2);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20, mass_mailing: true });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should not have any message initially in channel"
+ );
+
+ // insert some HTML in editable
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "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 on mailing channel 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);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20, mass_mailing: true });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "should not have any message initially in mailing channel"
+ );
+
+ // insert some HTML in editable
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "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"
+ );
+});
+
+QUnit.test('rendering of inbox message', async function (assert) {
+ // AKU TODO: kinda message specific test
+ assert.expect(7);
+
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ model: 'res.partner', // random existing model
+ needaction: true, // for message_fetch domain
+ needaction_partner_ids: [this.data.currentPartnerId], // for consistency
+ record_name: 'Refactoring', // random name, will be asserted in the test
+ res_id: 20, // random related id
+ });
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll('.o_Message').length,
+ 1,
+ "should display a message"
+ );
+ const message = document.querySelector('.o_Message');
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_originThread`).length,
+ 1,
+ "should display origin thread of message"
+ );
+ assert.strictEqual(
+ message.querySelector(`:scope .o_Message_originThread`).textContent,
+ " on Refactoring",
+ "should display origin thread name"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_command`).length,
+ 3,
+ "should display 3 commands"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_commandStar`).length,
+ 1,
+ "should display star command"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_commandReply`).length,
+ 1,
+ "should display reply command"
+ );
+ assert.strictEqual(
+ message.querySelectorAll(`:scope .o_Message_commandMarkAsRead`).length,
+ 1,
+ "should display mark as read command"
+ );
+});
+
+QUnit.test('mark channel as seen on last message visible [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(3);
+
+ // channel expected to be found in the sidebar, with the expected message_unread_counter
+ // and a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 10, message_unread_counter: 1 });
+ this.data['mail.message'].records.push({
+ id: 12,
+ body: "not empty",
+ channel_ids: [10],
+ model: 'mail.channel',
+ res_id: 10,
+ });
+ await this.start();
+ assert.containsOnce(
+ document.body,
+ `.o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]`,
+ "should have discuss sidebar item with the channel"
+ );
+ assert.hasClass(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `),
+ 'o-unread',
+ "sidebar item of channel ID 10 should be unread"
+ );
+
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-thread-last-seen-by-current-partner-message-id-changed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click();
+ },
+ message: "should wait until last seen by current partner message id changed",
+ predicate: ({ thread }) => {
+ return (
+ thread.id === 10 &&
+ thread.model === 'mail.channel' &&
+ thread.lastSeenByCurrentPartnerMessageId === 12
+ );
+ },
+ }));
+ assert.doesNotHaveClass(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `),
+ 'o-unread',
+ "sidebar item of channel ID 10 should not longer be unread"
+ );
+});
+
+QUnit.test('receive new needaction messages', async function (assert) {
+ assert.expect(12);
+
+ await this.start();
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `),
+ "should have inbox in sidebar"
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `).classList.contains('o-active'),
+ "inbox should be current discuss thread"
+ );
+ assert.notOk(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `),
+ "inbox item in sidebar should not have any counter"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_Message`).length,
+ 0,
+ "should have no messages in inbox initially"
+ );
+
+ // simulate receiving a new needaction message
+ await afterNextRender(() => {
+ const data = {
+ body: "not empty",
+ id: 100,
+ needaction_partner_ids: [3],
+ model: 'res.partner',
+ res_id: 20,
+ };
+ const notifications = [[['my-db', 'ir.needaction', 3], data]];
+ this.widget.call('bus_service', 'trigger', 'notification', notifications);
+ });
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `),
+ "inbox item in sidebar should now have counter"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).textContent,
+ '1',
+ "inbox item in sidebar should have counter of '1'"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_Message`).length,
+ 1,
+ "should have one message in inbox"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_Discuss_thread .o_Message`).dataset.messageLocalId,
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId,
+ "should display newly received needaction message"
+ );
+
+ // simulate receiving another new needaction message
+ await afterNextRender(() => {
+ const data2 = {
+ body: "not empty",
+ id: 101,
+ needaction_partner_ids: [3],
+ model: 'res.partner',
+ res_id: 20,
+ };
+ const notifications2 = [[['my-db', 'ir.needaction', 3], data2]];
+ this.widget.call('bus_service', 'trigger', 'notification', notifications2);
+ });
+ assert.strictEqual(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ .o_DiscussSidebarItem_counter
+ `).textContent,
+ '2',
+ "inbox item in sidebar should have counter of '2'"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_Message`).length,
+ 2,
+ "should have 2 messages in inbox"
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `),
+ "should still display 1st needaction message"
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId
+ }"]
+ `),
+ "should display 2nd needaction message"
+ );
+});
+
+QUnit.test('reply to message from inbox (message linked to document)', async function (assert) {
+ assert.expect(19);
+
+ // message that is expected to be found in Inbox
+ this.data['mail.message'].records.push({
+ body: "<p>Test</p>",
+ date: "2019-04-20 11:00:00",
+ id: 100, // random unique id, will be used to link notification to message
+ message_type: 'comment',
+ // needaction needs to be set here for message_fetch domain, because
+ // mocked models don't have computed fields
+ needaction: true,
+ model: 'res.partner',
+ record_name: 'Refactoring',
+ res_id: 20,
+ });
+ // notification to have message in Inbox
+ this.data['mail.notification'].records.push({
+ mail_message_id: 100, // id of related message
+ res_partner_id: this.data.currentPartnerId, // must be for current partner
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_post') {
+ assert.step('message_post');
+ assert.strictEqual(
+ args.model,
+ 'res.partner',
+ "should post message to record with model 'res.partner'"
+ );
+ assert.strictEqual(
+ args.args[0],
+ 20,
+ "should post message to record with Id 20"
+ );
+ assert.strictEqual(
+ args.kwargs.body,
+ "Test",
+ "should post with provided content in composer input"
+ );
+ assert.strictEqual(
+ args.kwargs.message_type,
+ "comment",
+ "should set message type as 'comment'"
+ );
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Message').length,
+ 1,
+ "should display a single message"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Message').dataset.messageLocalId,
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId,
+ "should display message with ID 100"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Message_originThread').textContent,
+ " on Refactoring",
+ "should display message originates from record 'Refactoring'"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Message_commandReply').click()
+ );
+ assert.ok(
+ document.querySelector('.o_Message').classList.contains('o-selected'),
+ "message should be selected after clicking on reply icon"
+ );
+ assert.ok(
+ document.querySelector('.o_Composer'),
+ "should have composer after clicking on reply to message"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_Composer_threadName`).textContent,
+ " on: Refactoring",
+ "composer should display origin thread name of message"
+ );
+ assert.strictEqual(
+ document.activeElement,
+ document.querySelector(`.o_ComposerTextInput_textarea`),
+ "composer text input should be auto-focus"
+ );
+
+ await afterNextRender(() =>
+ document.execCommand('insertText', false, "Test")
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_Composer_buttonSend').click()
+ );
+ assert.verifySteps(['message_post']);
+ assert.notOk(
+ document.querySelector('.o_Composer'),
+ "should no longer have composer after posting reply to message"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Message').length,
+ 1,
+ "should still display a single message after posting reply"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Message').dataset.messageLocalId,
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId,
+ "should still display message with ID 100 after posting reply"
+ );
+ assert.notOk(
+ document.querySelector('.o_Message').classList.contains('o-selected'),
+ "message should not longer be selected after posting reply"
+ );
+ assert.ok(
+ document.querySelector('.o_notification'),
+ "should display a notification after posting reply"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_notification_content').textContent,
+ "Message posted on \"Refactoring\"",
+ "notification should tell that message has been posted to the record 'Refactoring'"
+ );
+});
+
+QUnit.test('load recent messages from thread (already loaded some old messages)', async function (assert) {
+ assert.expect(6);
+
+ // channel expected to be found in the sidebar,
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ for (let i = 0; i < 50; i++) {
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ channel_ids: [20], // id of related channel
+ id: 100 + i, // random unique id, will be referenced in the test
+ model: 'mail.channel', // expected value to link message to channel
+ // needaction needs to be set here for message_fetch domain, because
+ // mocked models don't have computed fields
+ needaction: i === 0,
+ // the goal is to have only the first (oldest) message in Inbox
+ needaction_partner_ids: i === 0 ? [this.data.currentPartnerId] : [],
+ res_id: 20, // id of related channel
+ });
+ }
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll('.o_Message').length,
+ 1,
+ "Inbox should have a single message initially"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Message').dataset.messageLocalId,
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId,
+ "the only message initially should be the one marked as 'needaction'"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click();
+ },
+ message: "should wait until channel scrolled to bottom after opening it from the discuss sidebar",
+ predicate: ({ scrollTop, thread }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ thread &&
+ thread.model === 'mail.channel' &&
+ thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Message').length,
+ 31,
+ `should display 31 messages inside the channel after clicking on it (the previously known
+ message from Inbox and the 30 most recent messages that have been fetched)`
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `).length,
+ 1,
+ "should display the message from Inbox inside the channel too"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => document.querySelector('.o_Discuss_thread .o_ThreadView_messageList').scrollTop = 0,
+ message: "should wait until channel 20 loaded more messages after scrolling to top",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'more-messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20
+ );
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Message').length,
+ 50,
+ "should display 50 messages inside the channel after scrolling to load more (all messages fetched)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId
+ }"]
+ `).length,
+ 1,
+ "should still display the message from Inbox inside the channel too"
+ );
+});
+
+QUnit.test('messages marked as read move to "History" mailbox', async function (assert) {
+ assert.expect(10);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20 });
+ // expected messages
+ this.data['mail.message'].records.push(
+ {
+ body: "not empty",
+ id: 100, // random unique id, useful to link notification
+ model: 'mail.channel', // value to link message to channel
+ // needaction needs to be set here for message_fetch domain, because
+ // mocked models don't have computed fields
+ needaction: true,
+ res_id: 20, // id of related channel
+ },
+ {
+ body: "not empty",
+ id: 101, // random unique id, useful to link notification
+ model: 'mail.channel', // value to link message to channel
+ // needaction needs to be set here for message_fetch domain, because
+ // mocked models don't have computed fields
+ needaction: true,
+ res_id: 20, // id of related channel
+ }
+ );
+ this.data['mail.notification'].records.push(
+ // notification to have first message in inbox
+ {
+ mail_message_id: 100, // id of related message
+ res_partner_id: this.data.currentPartnerId, // must be for current partner
+ },
+ // notification to have second message in inbox
+ {
+ mail_message_id: 101, // id of related message
+ res_partner_id: this.data.currentPartnerId, // must be for current partner
+ }
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.box_history',
+ },
+ },
+ });
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).classList.contains('o-active'),
+ "history mailbox should be active thread"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_MessageList_empty`).length,
+ 1,
+ "should have empty thread in history"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `).click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `).classList.contains('o-active'),
+ "inbox mailbox should be active thread"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_MessageList_empty`).length,
+ 0,
+ "inbox mailbox should not be empty"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_MessageList_message`).length,
+ 2,
+ "inbox mailbox should have 2 messages"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_widget_Discuss_controlPanelButtonMarkAllRead').click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `).classList.contains('o-active'),
+ "inbox mailbox should still be active after mark as read"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_MessageList_empty`).length,
+ 1,
+ "inbox mailbox should now be empty after mark as read"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).classList.contains('o-active'),
+ "history mailbox should be active"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_MessageList_empty`).length,
+ 0,
+ "history mailbox should not be empty after mark as read"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_MessageList_message`).length,
+ 2,
+ "history mailbox should have 2 messages"
+ );
+});
+
+QUnit.test('mark a single message as read should only move this message to "History" mailbox', async function (assert) {
+ assert.expect(9);
+
+ this.data['mail.message'].records.push(
+ {
+ body: "not empty",
+ id: 1,
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ },
+ {
+ body: "not empty",
+ id: 2,
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ }
+ );
+ this.data['mail.notification'].records.push(
+ {
+ mail_message_id: 1,
+ res_partner_id: this.data.currentPartnerId,
+ },
+ {
+ mail_message_id: 2,
+ res_partner_id: this.data.currentPartnerId,
+ }
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.box_history',
+ },
+ },
+ });
+ assert.hasClass(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `),
+ 'o-active',
+ "history mailbox should initially be the active thread"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_MessageList_empty',
+ "history mailbox should initially be empty"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `).click()
+ );
+ assert.hasClass(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.inbox.localId
+ }"]
+ `),
+ 'o-active',
+ "inbox mailbox should be active thread after clicking on it"
+ );
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 2,
+ "inbox mailbox should have 2 messages"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 1 }).localId
+ }"] .o_Message_commandMarkAsRead
+ `).click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "inbox mailbox should have one less message after clicking mark as read"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 2 }).localId
+ }"]`,
+ "message still in inbox should be the one not marked as read"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).click()
+ );
+ assert.hasClass(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `),
+ 'o-active',
+ "history mailbox should be active after clicking on it"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "history mailbox should have only 1 message after mark as read"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Message[data-message-local-id="${
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 1 }).localId
+ }"]`,
+ "message moved in history should be the one marked as read"
+ );
+});
+
+QUnit.test('all messages in "Inbox" in "History" after marked all as read', async function (assert) {
+ assert.expect(2);
+
+ const messageOffset = 200;
+ for (let id = messageOffset; id < messageOffset + 40; id++) {
+ // message expected to be found in Inbox
+ this.data['mail.message'].records.push({
+ body: "not empty",
+ id, // will be used to link notification to message
+ // needaction needs to be set here for message_fetch domain, because
+ // mocked models don't have computed fields
+ needaction: true,
+ });
+ // notification to have message in Inbox
+ this.data['mail.notification'].records.push({
+ mail_message_id: id, // id of related message
+ res_partner_id: this.data.currentPartnerId, // must be for current partner
+ });
+
+ }
+ await this.start({
+ waitUntilEvent: {
+ eventName: 'o-component-message-list-scrolled',
+ message: "should wait until inbox scrolled to its last message initially",
+ predicate: ({ orderedMessages, scrollTop, thread }) => {
+ const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
+ return (
+ thread &&
+ thread.model === 'mail.box' &&
+ thread.id === 'inbox' &&
+ orderedMessages.length === 30 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ },
+ });
+
+ await afterNextRender(async () => {
+ const markAllReadButton = document.querySelector('.o_widget_Discuss_controlPanelButtonMarkAllRead');
+ markAllReadButton.click();
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "there should no message in Inbox anymore"
+ );
+
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebarItem[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).click();
+ },
+ message: "should wait until history scrolled to its last message after opening it from the discuss sidebar",
+ predicate: ({ orderedMessages, scrollTop, thread }) => {
+ const messageList = document.querySelector('.o_MessageList');
+ return (
+ thread &&
+ thread.model === 'mail.box' &&
+ thread.id === 'history' &&
+ orderedMessages.length === 30 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ });
+
+ // simulate a scroll to top to load more messages
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => document.querySelector('.o_MessageList').scrollTop = 0,
+ message: "should wait until mailbox history loaded more messages after scrolling to top",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'more-messages-loaded' &&
+ threadViewer.thread.model === 'mail.box' &&
+ threadViewer.thread.id === 'history'
+ );
+ },
+ });
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 40,
+ "there should be 40 messages in History"
+ );
+});
+
+QUnit.test('receive new chat message: out of odoo focus (notification, channel)', async function (assert) {
+ assert.expect(4);
+
+ // channel expected to be found in the sidebar
+ // with a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ id: 20, channel_type: 'chat' });
+ const bus = new Bus();
+ bus.on('set_title_part', null, payload => {
+ assert.step('set_title_part');
+ assert.strictEqual(payload.part, '_chat');
+ assert.strictEqual(payload.title, "1 Message");
+ });
+ await this.start({
+ env: { bus },
+ services: {
+ bus_service: BusService.extend({
+ _beep() {}, // Do nothing
+ _poll() {}, // Do nothing
+ _registerWindowUnload() {}, // Do nothing
+ isOdooFocused: () => false,
+ updateOption() {},
+ }),
+ },
+ });
+
+ // simulate receiving a new message with odoo focused
+ await afterNextRender(() => {
+ const messageData = {
+ channel_ids: [20],
+ id: 126,
+ model: 'mail.channel',
+ res_id: 20,
+ };
+ const notifications = [[['my-db', 'mail.channel', 20], messageData]];
+ this.widget.call('bus_service', 'trigger', 'notification', notifications);
+ });
+ assert.verifySteps(['set_title_part']);
+});
+
+QUnit.test('receive new chat message: out of odoo focus (notification, chat)', async function (assert) {
+ assert.expect(4);
+
+ // chat expected to be found in the sidebar with the proper channel_type
+ // and a random unique id that will be referenced in the test
+ this.data['mail.channel'].records.push({ channel_type: "chat", id: 10 });
+ const bus = new Bus();
+ bus.on('set_title_part', null, payload => {
+ assert.step('set_title_part');
+ assert.strictEqual(payload.part, '_chat');
+ assert.strictEqual(payload.title, "1 Message");
+ });
+ await this.start({
+ env: { bus },
+ services: {
+ bus_service: BusService.extend({
+ _beep() {}, // Do nothing
+ _poll() {}, // Do nothing
+ _registerWindowUnload() {}, // Do nothing
+ isOdooFocused: () => false,
+ updateOption() {},
+ }),
+ },
+ });
+
+ // simulate receiving a new message with odoo focused
+ await afterNextRender(() => {
+ const messageData = {
+ channel_ids: [10],
+ id: 126,
+ model: 'mail.channel',
+ res_id: 10,
+ };
+ const notifications = [[['my-db', 'mail.channel', 10], messageData]];
+ this.widget.call('bus_service', 'trigger', 'notification', notifications);
+ });
+ assert.verifySteps(['set_title_part']);
+});
+
+QUnit.test('receive new chat messages: out of odoo focus (tab title)', async function (assert) {
+ assert.expect(12);
+
+ let step = 0;
+ // channel and chat expected to be found in the sidebar
+ // with random unique id and name that will be referenced in the test
+ this.data['mail.channel'].records.push(
+ { channel_type: 'chat', id: 20, public: 'private' },
+ { channel_type: 'chat', id: 10, public: 'private' },
+ );
+ const bus = new Bus();
+ bus.on('set_title_part', null, payload => {
+ step++;
+ assert.step('set_title_part');
+ assert.strictEqual(payload.part, '_chat');
+ if (step === 1) {
+ assert.strictEqual(payload.title, "1 Message");
+ }
+ if (step === 2) {
+ assert.strictEqual(payload.title, "2 Messages");
+ }
+ if (step === 3) {
+ assert.strictEqual(payload.title, "3 Messages");
+ }
+ });
+ await this.start({
+ env: { bus },
+ services: {
+ bus_service: BusService.extend({
+ _beep() {}, // Do nothing
+ _poll() {}, // Do nothing
+ _registerWindowUnload() {}, // Do nothing
+ isOdooFocused: () => false,
+ updateOption() {},
+ }),
+ },
+ });
+
+ // simulate receiving a new message in chat 20 with odoo focused
+ await afterNextRender(() => {
+ const messageData1 = {
+ channel_ids: [20],
+ id: 126,
+ model: 'mail.channel',
+ res_id: 20,
+ };
+ const notifications1 = [[['my-db', 'mail.channel', 20], messageData1]];
+ this.widget.call('bus_service', 'trigger', 'notification', notifications1);
+ });
+ assert.verifySteps(['set_title_part']);
+
+ // simulate receiving a new message in chat 10 with odoo focused
+ await afterNextRender(() => {
+ const messageData2 = {
+ channel_ids: [10],
+ id: 127,
+ model: 'mail.channel',
+ res_id: 10,
+ };
+ const notifications2 = [[['my-db', 'mail.channel', 10], messageData2]];
+ this.widget.call('bus_service', 'trigger', 'notification', notifications2);
+ });
+ assert.verifySteps(['set_title_part']);
+
+ // simulate receiving another new message in chat 10 with odoo focused
+ await afterNextRender(() => {
+ const messageData3 = {
+ channel_ids: [10],
+ id: 128,
+ model: 'mail.channel',
+ res_id: 10,
+ };
+ const notifications3 = [[['my-db', 'mail.channel', 10], messageData3]];
+ this.widget.call('bus_service', 'trigger', 'notification', notifications3);
+ });
+ assert.verifySteps(['set_title_part']);
+});
+
+QUnit.test('auto-focus composer on opening thread', async function (assert) {
+ assert.expect(14);
+
+ // expected correspondent, with a random unique id that will be used to link
+ // partner to chat and a random name that will be asserted in the test
+ this.data['res.partner'].records.push({ id: 7, name: "Demo User" });
+ this.data['mail.channel'].records.push(
+ // channel expected to be found in the sidebar
+ {
+ id: 20, // random unique id, will be referenced in the test
+ name: "General", // random name, will be asserted in the test
+ },
+ // chat expected to be found in the sidebar
+ {
+ channel_type: 'chat', // testing a chat is the goal of the test
+ id: 10, // random unique id, will be referenced in the test
+ members: [this.data.currentPartnerId, 7], // expected partners
+ public: 'private', // expected value for testing a chat
+ }
+ );
+ await this.start();
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-name="Inbox"]
+ `).length,
+ 1,
+ "should have mailbox 'Inbox' in the sidebar"
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-name="Inbox"]
+ `).classList.contains('o-active'),
+ "mailbox 'Inbox' should be active initially"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-name="General"]
+ `).length,
+ 1,
+ "should have channel 'General' in the sidebar"
+ );
+ assert.notOk(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-name="General"]
+ `).classList.contains('o-active'),
+ "channel 'General' should not be active initially"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_DiscussSidebar_item[data-thread-name="Demo User"]
+ `).length,
+ 1,
+ "should have chat 'Demo User' in the sidebar"
+ );
+ assert.notOk(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-name="Demo User"]
+ `).classList.contains('o-active'),
+ "chat 'Demo User' should not be active initially"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Composer`).length,
+ 0,
+ "there should be no composer when active thread of discuss is mailbox 'Inbox'"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebar_item[data-thread-name="General"]`).click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-name="General"]
+ `).classList.contains('o-active'),
+ "channel 'General' should become active after selecting it from the sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Composer`).length,
+ 1,
+ "there should be a composer when active thread of discuss is channel 'General'"
+ );
+ assert.strictEqual(
+ document.activeElement,
+ document.querySelector(`.o_ComposerTextInput_textarea`),
+ "composer of channel 'General' should be automatically focused on opening"
+ );
+
+ document.querySelector(`.o_ComposerTextInput_textarea`).blur();
+ assert.notOk(
+ document.activeElement === document.querySelector(`.o_ComposerTextInput_textarea`),
+ "composer of channel 'General' should no longer focused on click away"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebar_item[data-thread-name="Demo User"]`).click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-name="Demo User"]
+ `).classList.contains('o-active'),
+ "chat 'Demo User' should become active after selecting it from the sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Composer`).length,
+ 1,
+ "there should be a composer when active thread of discuss is chat 'Demo User'"
+ );
+ assert.strictEqual(
+ document.activeElement,
+ document.querySelector(`.o_ComposerTextInput_textarea`),
+ "composer of chat 'Demo User' should be automatically focused on opening"
+ );
+});
+
+QUnit.test('mark channel as seen if last message is visible when switching channels when the previous channel had a more recent last message than the current channel [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(1);
+
+ this.data['mail.channel'].records.push(
+ { id: 10, message_unread_counter: 1, name: 'Bla' },
+ { id: 11, message_unread_counter: 1, name: 'Blu' },
+ );
+ this.data['mail.message'].records.push({
+ body: 'oldest message',
+ channel_ids: [10],
+ id: 10,
+ }, {
+ body: 'newest message',
+ channel_ids: [11],
+ id: 11,
+ });
+ await this.start({
+ discuss: {
+ context: {
+ active_id: 'mail.channel_11',
+ },
+ },
+ waitUntilEvent: {
+ eventName: 'o-thread-view-hint-processed',
+ message: "should wait until channel 11 loaded its messages initially",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 11 &&
+ hint.type === 'messages-loaded'
+ );
+ },
+ },
+ });
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-thread-last-seen-by-current-partner-message-id-changed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click();
+ },
+ message: "should wait until last seen by current partner message id changed",
+ predicate: ({ thread }) => {
+ return (
+ thread.id === 10 &&
+ thread.model === 'mail.channel' &&
+ thread.lastSeenByCurrentPartnerMessageId === 10
+ );
+ },
+ }));
+ assert.doesNotHaveClass(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `),
+ 'o-unread',
+ "sidebar item of channel ID 10 should no longer be unread"
+ );
+});
+
+QUnit.test('add custom filter should filter messages accordingly to selected filter', async function (assert) {
+ assert.expect(4);
+
+ this.data['mail.channel'].records.push({
+ id: 20,
+ name: "General"
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ const domainsAsStr = args.kwargs.domain.map(domain => domain.join(''));
+ assert.step(`message_fetch:${domainsAsStr.join(',')}`);
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.verifySteps(['message_fetch:needaction=true'], "A message_fetch request should have been done for needaction messages as inbox is selected by default");
+
+ // Open filter menu of control panel and select a custom filter (id = 0, the only one available)
+ await toggleFilterMenu(document.body);
+ await toggleAddCustomFilter(document.body);
+ await applyFilter(document.body);
+ assert.verifySteps(['message_fetch:id=0,needaction=true'], "A message_fetch request should have been done for selected filter & domain of current thread (inbox)");
+});
+
+});
+});
+});
+
+});