summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components/discuss/tests/discuss_tests.js
diff options
context:
space:
mode:
authorXavier Dubuc <xdu@odoo.com>2020-06-15 14:36:47 +0000
committerAlexandre Kühn <aku@odoo.com>2020-06-25 11:13:09 +0000
commite5de39d03669b98224e2ebc88bfc2003a64e1550 (patch)
tree4fe911b033ffd9c39ab70e8325970b2b4bc605e1 /addons/mail/static/src/components/discuss/tests/discuss_tests.js
parent281329361dccd68895f39a69652329420fb52014 (diff)
[FIX] mail: fix disappearing moderated messages
task-2277748 task-2277811 closes odoo/odoo#53652 X-original-commit: 5ef6aedb8271fa05f79fb6f93f679caa9cd418ff Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Diffstat (limited to 'addons/mail/static/src/components/discuss/tests/discuss_tests.js')
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_tests.js4573
1 files changed, 4573 insertions, 0 deletions
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 00000000000..994d75a8935
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
@@ -0,0 +1,4573 @@
+odoo.define('mail/static/src/components/discuss/tests/discuss_tests.js', function (require) {
+'use strict';
+
+const BusService = require('bus.BusService');
+
+const {
+ afterEach: utilsAfterEach,
+ afterNextRender,
+ beforeEach: utilsBeforeEach,
+ inputFiles,
+ start: utilsStart,
+} = require('mail/static/src/utils/test_utils.js');
+
+const Bus = require('web.Bus');
+const { makeTestPromise, file: { createFile } } = require('web.test_utils');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('discuss', {}, function () {
+QUnit.module('discuss_tests.js', {
+ beforeEach() {
+ utilsBeforeEach(this);
+
+ this.start = async params => {
+ if (this.widget) {
+ this.widget.destroy();
+ }
+ let { env, widget } = await utilsStart(Object.assign({}, params, {
+ autoOpenDiscuss: true,
+ data: this.data,
+ hasDiscuss: true,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ if (this.widget) {
+ this.widget.destroy();
+ }
+ utilsAfterEach(this);
+ },
+});
+
+QUnit.test('messaging not initialized', async function (assert) {
+ assert.expect(1);
+
+ await this.start({
+ async mockRPC(route) {
+ if (route === '/mail/init_messaging') {
+ // simulate messaging never initialized
+ return new Promise(resolve => {});
+ }
+ return this._super(...arguments);
+ },
+ waitUntilMessagingInitialized: false,
+ });
+ 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();
+ },
+ waitUntilMessagingInitialized: false,
+ });
+ 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_ThreadViewer'),
+ "thread section should use ThreadViewer 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);
+
+ Object.assign(this.data.initMessaging, {
+ needaction_inbox_counter: 100,
+ });
+ 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,
+ "100",
+ "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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ 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'].find(thread =>
+ thread.id === 20 &&
+ thread.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_ThreadViewer_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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ message_needaction_counter: 10,
+ }],
+ },
+ });
+ 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,
+ "10",
+ "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: public/private channel rendering', async function (assert) {
+ assert.expect(5);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 100,
+ name: "channel1",
+ public: 'public',
+ }],
+ channel_private_group: [{
+ channel_type: "channel",
+ 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'].find(thread =>
+ thread.id === 100 &&
+ thread.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'].find(thread =>
+ thread.id === 101 &&
+ thread.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'].find(thread =>
+ thread.id === 100 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `);
+ const channel2 = document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 101 &&
+ thread.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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_direct_message: [{
+ channel_type: "chat",
+ direct_partner: [{
+ id: 7,
+ name: "Demo",
+ }],
+ id: 10,
+ }],
+ },
+ });
+ 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'].find(thread =>
+ thread.id === 10 &&
+ thread.model === 'mail.channel'
+ ).localId,
+ "should have chat with Id 20"
+ );
+ 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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_direct_message: [{
+ channel_type: "chat",
+ direct_partner: [{
+ id: 7,
+ name: "Demo",
+ }],
+ id: 10,
+ message_unread_counter: 100,
+ }],
+ },
+ });
+ 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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_direct_message: [{
+ channel_type: "chat",
+ direct_partner: [{
+ id: 101,
+ im_status: 'offline',
+ name: "Partner1",
+ }],
+ id: 11,
+ }, {
+ channel_type: "chat",
+ direct_partner: [{
+ id: 102,
+ im_status: 'online',
+ name: "Partner2",
+ }],
+ id: 12,
+ }, {
+ channel_type: "chat",
+ direct_partner: [{
+ id: 103,
+ im_status: 'away',
+ name: "Partner3",
+ }],
+ id: 13,
+ }],
+ },
+ });
+ 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'].find(thread =>
+ thread.id === 11 &&
+ thread.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'].find(thread =>
+ thread.id === 12 &&
+ thread.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'].find(thread =>
+ thread.id === 13 &&
+ thread.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'].find(thread =>
+ thread.id === 11 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `);
+ const chat2 = document.querySelector(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 12 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `);
+ const chat3 = document.querySelector(`
+ .o_DiscussSidebar_groupChat
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 13 &&
+ thread.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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_direct_message: [{
+ channel_type: "chat",
+ custom_channel_name: "Marc",
+ direct_partner: [{
+ id: 7,
+ name: "Marc Demo",
+ }],
+ id: 10,
+ }],
+ },
+ });
+ 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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_direct_message: [{
+ custom_channel_name: "Marc",
+ channel_type: "chat",
+ direct_partner: [{
+ id: 7,
+ name: "Marc Demo",
+ }],
+ id: 10,
+ }],
+ },
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'channel_set_custom_name') {
+ return;
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ 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'].find(thread =>
+ thread.id === 20 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `).length,
+ 1,
+ "should have 'general' channel 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_ThreadViewer_messageList .o_MessageList_empty
+ `).length,
+ 1,
+ "should have empty thread in inbox"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadViewer_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_ThreadViewer_messageList .o_MessageList_empty
+ `).length,
+ 1,
+ "should have empty thread in starred"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadViewer_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_ThreadViewer_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'].find(thread =>
+ thread.id === 20 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `).click()
+ );
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 20 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `).classList.contains('o-active'),
+ "channel 'general' should be active thread"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_empty
+ `).length,
+ 1,
+ "should have empty thread in starred"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadViewer_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(5);
+
+ 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.strictEqual(
+ args.args.length,
+ 1,
+ "should have a single item in args"
+ );
+ assert.deepEqual(
+ args.args[0],
+ [["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(8);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ 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.strictEqual(
+ args.args.length,
+ 1,
+ "should have a single item in args"
+ );
+ assert.deepEqual(
+ args.args[0],
+ [["channel_ids", "in", [20]]],
+ "should fetch messages from channel"
+ );
+ return [{
+ author_id: [11, "Demo"],
+ body: "<p>body</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ }];
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Discuss_thread .o_ThreadViewer_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'].find(message => message.id === 100).localId
+ }"]
+ `).length,
+ 1,
+ "should have message with Id 100"
+ );
+});
+
+QUnit.test('basic rendering of message', async function (assert) {
+ // AKU TODO: should be in message-only tests
+ assert.expect(13);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [{
+ author_id: [11, "Demo"],
+ body: "<p>body</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ }];
+ }
+ return this._super(...arguments);
+ },
+ });
+ const message = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadViewer_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [{
+ author_id: [11, "Demo"],
+ body: "<p>body1</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ }, {
+ author_id: [11, "Demo"],
+ body: "<p>body2</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:30",
+ id: 101,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ }];
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_message
+ `).length,
+ 2,
+ "should have 2 messages"
+ );
+ const message1 = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadViewer_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 100).localId
+ }"]
+ `);
+ const message2 = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadViewer_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.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);
+
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ // fetching messages from inbox
+ return [{
+ author_id: [11, "Demo"],
+ body: "<p>body1</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ needaction: true,
+ needaction_partner_ids: [3],
+ record_name: 'General',
+ res_id: 20,
+ }, {
+ author_id: [11, "Demo"],
+ body: "<p>body2</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:30",
+ id: 101,
+ message_type: 'comment',
+ model: 'mail.channel',
+ needaction: true,
+ needaction_partner_ids: [3],
+ record_name: 'General',
+ res_id: 20,
+ }];
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_message
+ `).length,
+ 2,
+ "should have 2 messages"
+ );
+ const message1 = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadViewer_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 100).localId
+ }"]
+ `);
+ const message2 = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadViewer_messageList
+ .o_MessageList_message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ 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");
+ let messagesData = [];
+ // 29 messages
+ for (let i = 28; i >= 0; i--) {
+ messagesData.push({
+ author_id: [10 + i, `User${i}`],
+ body: `<p>body${i}</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100 + i,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ });
+ }
+ return messagesData;
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_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_ThreadViewer_messageList .o_MessageList_separatorLabelDate
+ `).textContent,
+ "April 20, 2019",
+ "should display date day of messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_message
+ `).length,
+ 29,
+ "should have 29 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_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(8);
+
+ let step = 0;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ step++;
+ if (step === 1) {
+ // fetching messages from channel (initial load)
+ assert.strictEqual(
+ args.kwargs.limit,
+ 30,
+ "should fetch up to 30 messages"
+ );
+ let messagesData = [];
+ // 30 messages
+ for (let i = 39; i >= 10; i--) {
+ messagesData.push({
+ author_id: [10 + i, `User${i}`],
+ body: `<p>body${i}</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100 + i,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ });
+ }
+ return messagesData;
+ }
+ if (step === 2) {
+ // fetching more messages from channel (load more)
+ assert.strictEqual(
+ args.kwargs.limit,
+ 30,
+ "should fetch up to 30 messages"
+ );
+ let messagesData = [];
+ // 10 messages
+ for (let i = 9; i >= 0; i--) {
+ messagesData.push({
+ author_id: [10 + i, `User${i}`],
+ body: `<p>body${i}</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100 + i,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ });
+ }
+ return messagesData;
+ }
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_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_ThreadViewer_messageList .o_MessageList_separatorLabelDate
+ `).textContent,
+ "April 20, 2019",
+ "should display date day of messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_message
+ `).length,
+ 30,
+ "should have 30 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_loadMore
+ `).length,
+ 1,
+ "should have load more link"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_loadMore
+ `).click()
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_message
+ `).length,
+ 40,
+ "should have 40 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ let messagesData = [];
+ // 25 messages
+ for (let i = 1; i <= 25; i++) {
+ messagesData.push({
+ author_id: [10 + i, `User${i}`],
+ body: `<p>body${i}</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100 + i,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ });
+ }
+ return messagesData;
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_message
+ `).length,
+ 25,
+ "should have 25 messages"
+ );
+ const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadViewer_messageList`);
+ assert.strictEqual(
+ messageList.scrollTop + messageList.clientHeight,
+ messageList.scrollHeight,
+ "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);
+
+ let step = 0;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ step++;
+ if (step === 1) {
+ // fetching messages from channel (initial load)
+ let messagesData = [];
+ // 30 messages
+ for (let i = 39; i >= 10; i--) {
+ messagesData.push({
+ author_id: [10 + i, `User${i}`],
+ body: `<p>body${i}</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100 + i,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ });
+ }
+ return messagesData;
+ }
+ if (step === 2) {
+ // fetching more messages from channel (load more)
+ let messagesData = [];
+ // 10 messages
+ for (let i = 9; i >= 0; i--) {
+ messagesData.push({
+ author_id: [10 + i, `User${i}`],
+ body: `<p>body${i}</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100 + i,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ });
+ }
+ return messagesData;
+ }
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_message
+ `).length,
+ 30,
+ "should have 30 messages"
+ );
+
+ await afterNextRender(() => {
+ document.querySelector(`.o_Discuss_thread .o_ThreadViewer_messageList`).scrollTop = 0;
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_message
+ `).length,
+ 40,
+ "should have 40 messages"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Dsiscuss_thread .o_ThreadViewer_messageList .o_MessageList_loadMore
+ `).length,
+ 0,
+ "should not longer have load more link (all messages loaded)"
+ );
+});
+
+QUnit.test('new messages separator', 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);
+
+ let step = 0;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ message_unread_counter: 0,
+ name: "General",
+ seen_message_id: 125,
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ step++;
+ if (step === 1) {
+ // fetching messages from channel (initial load)
+ let messagesData = [];
+ // 25 messages
+ for (let i = 1; i <= 25; i++) {
+ messagesData.push({
+ author_id: [10 + i, `User${i}`],
+ body: `<p>body${i}</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100 + i,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ });
+ }
+ return messagesData;
+ }
+ if (step === 2) {
+ throw new Error("should not fetch more messages");
+ }
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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
+ document.querySelector(`.o_Discuss_thread .o_ThreadViewer_messageList`).scrollTop = 0;
+ // composer is focused by default, we remove that focus
+ document.querySelector('.o_ComposerTextInput_textarea').blur();
+ // simulate receiving a new message
+ const data = {
+ author_id: [36, "User26"],
+ body: "<p>body26</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 126,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ };
+ await afterNextRender(() => {
+ const notifications = [[['my-db', 'mail.channel', 20], data]];
+ this.widget.call('bus_service', 'trigger', 'notification', notifications)
+ });
+ 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 afterNextRender(() => {
+ document.querySelector(`.o_Discuss_thread .o_ThreadViewer_messageList`).scrollTop =
+ document.querySelector(`.o_Discuss_thread .o_ThreadViewer_messageList`).scrollHeight;
+ });
+
+ 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(4);
+
+ let step = 0;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 1,
+ name: "channel1",
+ }, {
+ channel_type: "channel",
+ id: 2,
+ name: "channel2",
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_1',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ step++;
+ if (step === 1) {
+ // fetching messages from channel1 (initial load)
+ let messagesData = [];
+ // 25 messages
+ for (let i = 1; i <= 25; i++) {
+ messagesData.push({
+ author_id: [10 + i, `User${i}`],
+ body: `<p>body${i}</p>`,
+ channel_ids: [1],
+ date: "2019-04-20 10:00:00",
+ id: 100 + i,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'channel1',
+ res_id: 1,
+ });
+ }
+ return messagesData;
+ }
+ if (step === 2) {
+ // fetching messages from channel2 (initial load)
+ let messagesData = [];
+ // 25 messages
+ for (let i = 1; i <= 25; i++) {
+ messagesData.push({
+ author_id: [10 + i, `User${i}`],
+ body: `<p>body${i}</p>`,
+ channel_ids: [2],
+ date: "2019-04-20 10:00:00",
+ id: 200 + i,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'channel2',
+ res_id: 2,
+ });
+ }
+ return messagesData;
+ }
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadViewer_messageList .o_MessageList_message
+ `).length,
+ 25,
+ "should have 25 messages"
+ );
+
+ // scroll to top of channel1
+ await afterNextRender(() => {
+ document.querySelector(`.o_Discuss_thread .o_ThreadViewer_messageList`).scrollTop = 0;
+ });
+ assert.strictEqual(
+ document.querySelector(`.o_Discuss_thread .o_ThreadViewer_messageList`).scrollTop,
+ 0,
+ "should have scrolled to top of thread"
+ );
+
+ // select channel2
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 2 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `).click()
+ );
+ // select channel1
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 1 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `).click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_Discuss_thread .o_ThreadViewer_messageList`).scrollTop,
+ 0,
+ "should have recovered scroll position of channel1 (scroll to top)"
+ );
+
+ // select channel2
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 2 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `).click()
+ );
+ const messageList = document.querySelector(`
+ .o_Discuss_thread
+ .o_ThreadViewer_messageList
+ `);
+ assert.strictEqual(
+ messageList.scrollTop + messageList.clientHeight,
+ messageList.scrollHeight,
+ "should have recovered scroll position of channel2 (scroll to bottom)"
+ );
+});
+
+QUnit.test('message origin redirect to channel', async function (assert) {
+ assert.expect(15);
+
+ let step = 0;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: 'channel',
+ id: 1,
+ name: "channel1",
+ }, {
+ channel_type: 'channel',
+ id: 2,
+ name: "channel2",
+ }],
+ },
+ });
+ let messagesData = [{
+ author_id: [10, "User1"],
+ body: `<p>message1</p>`,
+ channel_ids: [1, 2],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: "channel1",
+ res_id: 1,
+ }, {
+ author_id: [11, "User2"],
+ body: `<p>message2</p>`,
+ channel_ids: [1, 2],
+ date: "2019-04-20 10:00:00",
+ id: 101,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: "channel2",
+ res_id: 2,
+ }];
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_1',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ step++;
+ if (step === 1) {
+ // fetching messages from channel1 (initial load)
+ return messagesData;
+ }
+ if (step === 2) {
+ // fetching messages from channel2 (initial load)
+ return messagesData;
+ }
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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'].find(message => message.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'].find(message => message.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'].find(message => message.id === 100).localId
+ }"]
+ .o_Message_originThread
+ `).length,
+ 0,
+ "message1 should not have origin part in channel1 (same origin as channel)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 101).localId
+ }"]
+ .o_Message_originThread
+ `).length,
+ 1,
+ "message2 should have origin part (origin is channel2 !== channel1)"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 101).localId
+ }"]
+ .o_Message_originThread
+ `).textContent.trim(),
+ "(from #channel2)",
+ "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'].find(message => message.id === 101).localId
+ }"]
+ .o_Message_originThreadLink
+ `).length,
+ 1,
+ "message2 should have link to redirect to origin"
+ );
+
+ // click on origin link of message2 (= channel2)
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.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'].find(thread =>
+ thread.id === 2 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ .o_DiscussSidebarItem_activeIndicator
+ `).classList.contains('o-item-active'),
+ "channel2 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'].find(message => message.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'].find(message => message.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'].find(message => message.id === 100).localId
+ }"]
+ .o_Message_originThread
+ `).length,
+ 1,
+ "message1 should have origin thread part (= channel1 !== channel2)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 101).localId
+ }"]
+ .o_Message_originThread
+ `).length,
+ 0,
+ "message2 should not have origin thread part in channel2 (same as current channel)"
+ );
+ assert.strictEqual(
+ document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 100).localId
+ }"]
+ .o_Message_originThread
+ `).textContent.trim(),
+ "(from #channel1)",
+ "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'].find(message => message.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(9);
+
+ let step = 0;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 1,
+ name: "General",
+ }],
+ channel_direct_message: [{
+ channel_type: "chat",
+ direct_partner: [{
+ id: 7,
+ name: "Demo",
+ }],
+ id: 10,
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_1',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ step++;
+ if (step === 1) {
+ // fetching messages from General (initial load)
+ return [{
+ author_id: [7, "Demo"],
+ body: `<p>message1</p>`,
+ channel_ids: [1],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: "General",
+ res_id: 1,
+ }, {
+ author_id: [3, "Me"],
+ body: `<p>message2</p>`,
+ channel_ids: [1],
+ date: "2019-04-20 10:00:00",
+ id: 101,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: "General",
+ res_id: 1,
+ }];
+ }
+ if (step === 2) {
+ // fetching messages from DM (initial load)
+ return [];
+ }
+ }
+ if (args.model === 'res.users' && args.method === 'search') {
+ return [2];
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.ok(
+ document.querySelector(`
+ .o_DiscussSidebar_groupChannel
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 1 &&
+ thread.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'].find(thread =>
+ thread.id === 10 &&
+ thread.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,
+ 2,
+ "should have 2 messages"
+ );
+ const msg1 = document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 100).localId
+ }"]
+ `);
+ const msg2 = document.querySelector(`
+ .o_Discuss_thread
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 101).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"
+ );
+ assert.strictEqual(
+ msg2.querySelectorAll(`:scope .o_Message_authorAvatar`).length,
+ 1,
+ "message2 should have author image"
+ );
+ assert.notOk(
+ msg2.querySelector(`:scope .o_Message_authorAvatar`).classList.contains('o_redirect'),
+ "message2 should not have redirect to author (self-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'].find(thread =>
+ thread.id === 1 &&
+ thread.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'].find(thread =>
+ thread.id === 10 &&
+ thread.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);
+
+ let channelsData = [];
+ for (let id = 1; id <= 20; id++) {
+ channelsData.push({
+ channel_type: 'channel',
+ id,
+ name: `channel${id}`,
+ });
+ }
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: channelsData,
+ },
+ });
+ 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'].find(thread =>
+ thread.id === 12 &&
+ thread.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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ 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'].find(thread =>
+ thread.id === 20 &&
+ thread.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);
+
+ const self = this;
+ Object.assign(this.data.initMessaging, {
+ needaction_inbox_counter: 2,
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ message_needaction_counter: 2,
+ name: "General",
+ }],
+ },
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [{
+ author_id: [7, "Demo"],
+ body: `<p>message1</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ needaction: true,
+ needaction_partner_ids: [3],
+ record_name: "General",
+ res_id: 20,
+ }, {
+ author_id: [8, "Other"],
+ body: `<p>message2</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 101,
+ message_type: 'comment',
+ model: 'mail.channel',
+ needaction: true,
+ needaction_partner_ids: [3],
+ record_name: "General",
+ res_id: 20,
+ }];
+ }
+ if (args.method === 'mark_all_as_read') {
+ // simulate mark as read notification
+ const data = {
+ message_ids: [100, 101],
+ type: 'mark_as_read',
+ };
+ const notifications = [[['my-db', 'res.partner'], data]];
+ self.widget.call('bus_service', 'trigger', 'notification', notifications);
+ return;
+
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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'].find(thread =>
+ thread.id === 20 &&
+ thread.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'].find(thread =>
+ thread.id === 20 &&
+ thread.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);
+
+ const self = this;
+ Object.assign(this.data.initMessaging, { starred_counter: 2 });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.box_starred',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [{
+ author_id: [7, "Demo"],
+ body: `<p>message1</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: "General",
+ res_id: 20,
+ starred: true,
+ starred_partner_ids: [3],
+ }, {
+ author_id: [8, "Other"],
+ body: `<p>message2</p>`,
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 101,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: "General",
+ res_id: 20,
+ starred: true,
+ starred_partner_ids: [3],
+ }];
+ }
+ if (args.method === 'unstar_all') {
+ // simulate toggle_star notification
+ const data = {
+ message_ids: [100, 101],
+ starred: false,
+ type: 'toggle_star',
+ };
+ const notifications = [[['my-db', 'res.partner'], data]];
+ self.widget.call('bus_service', 'trigger', 'notification', notifications);
+ return;
+
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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);
+
+ const self = this;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ let messageData = {
+ author_id: [11, "Demo"],
+ body: "<p>body</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 20,
+ starred: false,
+ starred_partner_ids: [],
+ };
+
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [messageData];
+ }
+ 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"
+ );
+ // simulate toggle_star notification
+ messageData.starred = !messageData.starred;
+ const data = {
+ message_ids: [100],
+ starred: messageData.starred,
+ type: 'toggle_star',
+ };
+ const notifications = [[['my-db', 'res.partner'], data]];
+ self.widget.call('bus_service', 'trigger', 'notification', notifications);
+ return;
+ }
+ 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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: 'channel',
+ id: 20,
+ name: "General",
+ }, {
+ channel_type: 'channel',
+ id: 21,
+ name: "Special",
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [];
+ }
+ return this._super(...arguments);
+ },
+ });
+ // 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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: 'channel',
+ id: 20,
+ name: "General",
+ }, {
+ channel_type: 'channel',
+ id: 21,
+ name: "Special",
+ }],
+ },
+ });
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [];
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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'].find(attachment => attachment.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'].find(attachment => attachment.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'].find(attachment => attachment.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'].find(attachment => attachment.id === 4).localId,
+ "should have attachment with id 4 as 3rd attachment"
+ );
+});
+
+QUnit.test('post a simple message', async function (assert) {
+ assert.expect(15);
+
+ const self = this;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ name: "General",
+ }],
+ },
+ });
+ let messagesData = [];
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_20',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return messagesData;
+ }
+ 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'"
+ );
+ // simulate receiving a new message
+ const data = {
+ author_id: [3, "Admin"],
+ body: args.kwargs.body,
+ channel_ids: [20],
+ date: "2019-04-20 11:00:00",
+ id: 101,
+ message_type: args.kwargs.message_type,
+ model: 'mail.channel',
+ subtype_xmlid: args.kwargs.subtype_xmlid,
+ record_name: 'General',
+ res_id: 20,
+ };
+ const notifications = [
+ [['my-db', 'mail.channel', 20], data]
+ ];
+ messagesData.push(data);
+ self.widget.call('bus_service', 'trigger', 'notification', notifications);
+ return;
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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
+ 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_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter' }))
+ );
+ 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'].find(message => message.id === 101).localId,
+ "new message in thread should be linked to newly created message from message post"
+ );
+ assert.strictEqual(
+ message.querySelector(`:scope .o_Message_authorName`).textContent,
+ "Admin",
+ "new message in thread should be from Admin"
+ );
+ assert.strictEqual(
+ message.querySelector(`:scope .o_Message_content`).textContent,
+ "Test",
+ "new message in thread should have content typed from composer text input"
+ );
+});
+
+QUnit.test('rendering of inbox message', async function (assert) {
+ // AKU TODO: kinda message specific test
+ assert.expect(7);
+
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [{
+ author_id: [11, "Demo"],
+ body: "<p>body</p>",
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'project.task',
+ needaction_partner_ids: [44],
+ record_name: 'Refactoring',
+ res_id: 20,
+ }];
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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', async function (assert) {
+ assert.expect(3);
+
+ let step = 0;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: 'channel',
+ id: 10,
+ message_unread_counter: 1,
+ name: "General",
+ }],
+ },
+ });
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ step++;
+ if (step === 1) {
+ return [];
+ }
+ if (step === 2) {
+ return [{
+ author_id: [11, "Demo"],
+ body: "<p>body</p>",
+ date: "2019-04-20 10:00:00",
+ id: 100,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: "General",
+ res_id: 10,
+ }];
+ }
+ throw new Error("should not call 'message_fetch' more than twice");
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ `.o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 10 &&
+ thread.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'].find(thread =>
+ thread.id === 10 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `),
+ 'o-unread',
+ "sidebar item of channel ID 10 should be unread"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 10 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `).click()
+ );
+ assert.doesNotHaveClass(
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 10 &&
+ thread.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({
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [];
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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 = {
+ author_id: [7, "Demo"],
+ body: "<p>Test</p>",
+ date: "2019-04-20 11:00:00",
+ id: 100,
+ message_type: 'comment',
+ needaction_partner_ids: [3],
+ model: 'project.task',
+ record_name: 'Refactoring',
+ 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'].find(message => message.id === 100).localId,
+ "should display newly received needaction message"
+ );
+
+ // simulate receiving another new needaction message
+ await afterNextRender(() => {
+ const data2 = {
+ author_id: [7, "Demo"],
+ body: "<p>Test2</p>",
+ date: "2019-04-20 11:00:00",
+ id: 101,
+ message_type: 'comment',
+ needaction_partner_ids: [3],
+ model: 'project.task',
+ record_name: 'Refactoring',
+ 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'].find(message => message.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'].find(message => message.id === 101).localId
+ }"]
+ `),
+ "should display 2nd needaction message"
+ );
+});
+
+QUnit.test('reply to message from inbox (message linked to document)', async function (assert) {
+ assert.expect(20);
+
+ Object.assign(this.data.initMessaging, { needaction_inbox_counter: 1 });
+
+ let messagesData = [];
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ return [{
+ author_id: [7, "Demo"],
+ body: "<p>Test</p>",
+ date: "2019-04-20 11:00:00",
+ id: 100,
+ message_type: 'comment',
+ needaction_partner_ids: [3],
+ model: 'project.task',
+ record_name: 'Refactoring',
+ res_id: 20,
+ }];
+ }
+ if (args.method === 'message_post') {
+ assert.step('message_post');
+ assert.strictEqual(
+ args.model,
+ 'project.task',
+ "should post message to record with model 'project.task'"
+ );
+ 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'"
+ );
+ assert.strictEqual(
+ args.kwargs.subtype_xmlid,
+ "mail.mt_comment",
+ "should set subtype_xmlid as 'comment'"
+ );
+ messagesData.push({
+ author_id: [3, "Admin"],
+ body: args.kwargs.body,
+ date: "2019-04-20 11:00:00",
+ id: 101,
+ message_type: args.kwargs.message_type,
+ model: args.model,
+ subtype_xmlid: args.kwargs.subtype_xmlid,
+ record_name: 'Refactoring',
+ res_id: 20,
+ });
+ return;
+ }
+ if (args.method === 'message_format') {
+ return messagesData;
+ }
+ 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'].find(message => message.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");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter' }));
+ });
+ 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'].find(message => message.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(17);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: 'channel',
+ id: 20,
+ name: "General",
+ }],
+ },
+ needaction_inbox_counter: 1,
+ });
+ let step = 0;
+ let mailMessages = {};
+ for (let i = 0; i < 50; i++) {
+ mailMessages[100 + i] = {
+ author_id: [7, "Demo"],
+ body: `<p>Test${i}</p>`,
+ date: `2019-04-20 11:00:${i < 10 ? '0' + i : i}`,
+ id: 100 + i,
+ message_type: 'comment',
+ needaction_partner_ids: i === 0 ? [3] : [],
+ model: 'mail.channel',
+ record_name: "General",
+ res_id: 20,
+ };
+ }
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ step++;
+ if (step === 1) {
+ assert.step('message_fetch:load_inbox');
+ assert.deepEqual(
+ args.args[0],
+ [['needaction', '=', true]],
+ "should fetch needaction messages from inbox"
+ );
+ const needactionMessages = Object
+ .values(mailMessages)
+ .filter(mailMessage =>
+ mailMessage.needaction_partner_ids.length > 0
+ );
+ return needactionMessages;
+ }
+ if (step === 2) {
+ assert.step('message_fetch:load_channel_20');
+ assert.deepEqual(
+ args.args[0],
+ [['channel_ids', 'in', [20]]],
+ "should fetch messages from channel ID 20"
+ );
+ assert.strictEqual(
+ args.kwargs.limit,
+ 30,
+ "should limit fetch to 30 messages (load)"
+ );
+ const moreRecentMessages = Object
+ .values(mailMessages)
+ .sort((mailMsg1, mailMsg2) => mailMsg1.id > mailMsg2.id ? -1 : 1)
+ .splice(0, 30);
+ return moreRecentMessages;
+ }
+ if (step === 3) {
+ assert.step('message_fetch:load_more_channel_20');
+ assert.deepEqual(
+ args.args[0],
+ // loaded 30 messages, so 150 - 30 = 120 = ID of last message fetched
+ // => load more messages starting from this ID
+ [['id', '<', 120], ['channel_ids', 'in', [20]]],
+ "should fetch more messages from 30 more recent messages"
+ );
+ assert.strictEqual(
+ args.kwargs.limit,
+ 30,
+ "should limit fetch to 30 messages (load more)"
+ );
+ const loadMoreMessages = Object
+ .values(mailMessages)
+ .filter(mailMessage => mailMessage.id < 120)
+ .sort((mailMsg1, mailMsg2) => mailMsg1.id > mailMsg2.id ? -1 : 1)
+ .splice(0, 30);
+ return loadMoreMessages;
+ }
+ throw new Error("should not fetch more than twice");
+ }
+ return this._super(...arguments);
+ },
+ });
+ assert.verifySteps(
+ ['message_fetch:load_inbox'],
+ "should initially have fetched messages from inbox"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Message').length,
+ 1,
+ "should fetch a single message from inbox"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Message').dataset.messageLocalId,
+ this.env.models['mail.message'].find(message => message.id === 100).localId,
+ "should have fetched 1st message of channel 'General' as needaction from inbox"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 20 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `).click()
+ );
+ assert.verifySteps(
+ ['message_fetch:load_channel_20'],
+ "should initially have fetched messages from channel 'General' (channel 20)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Message').length,
+ 31,
+ "should display 30 messages from channel 'General' (only fetched ones)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 100).localId
+ }"]
+ `).length,
+ 1,
+ "should not display 1st message of 'General' as needaction from inbox"
+ );
+
+ await afterNextRender(() => {
+ document.querySelector(`.o_Discuss_thread .o_ThreadViewer_messageList`).scrollTop = 0;
+ });
+ assert.verifySteps(
+ ['message_fetch:load_more_channel_20'],
+ "should have fetched more messages from channel 'General' (channel 20)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Message').length,
+ 50,
+ "should display 50 messages from channel 'General' (all fetched)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(message => message.id === 100).localId
+ }"]
+ `).length,
+ 1,
+ "should include 1st message of 'General' as needaction from inbox in the fetched more messages"
+ );
+});
+
+QUnit.test('messages marked as read move to "History" mailbox', async function (assert) {
+ assert.expect(10);
+
+ const self = this;
+ this.data['mail.message'].records = [{
+ author_id: [5, 'Demo User'],
+ body: '<p>test 1</p>',
+ id: 1,
+ needaction: true,
+ needaction_partner_ids: [3],
+ }, {
+ author_id: [6, 'Test User'],
+ body: '<p>test 2</p>',
+ id: 2,
+ needaction: true,
+ needaction_partner_ids: [3],
+ }];
+ this.data['mail.notification'].records = [{
+ id: 50,
+ is_read: false,
+ mail_message_id: 1,
+ res_partner_id: 3,
+ }, {
+ id: 51,
+ is_read: false,
+ mail_message_id: 1,
+ res_partner_id: 3,
+ }];
+ this.data.initMessaging.needaction_inbox_counter = 2;
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.box_history',
+ },
+ },
+ async mockRPC(route, args) {
+ if (args.method === 'mark_all_as_read') {
+ for (const message of this.data['mail.message'].records) {
+ message.history_partner_ids = [3];
+ message.needaction_partner_ids = [];
+ }
+ const notificationData = {
+ type: 'mark_as_read',
+ message_ids: [1, 2],
+ };
+ const notification = [[false, 'res.partner', 3], notificationData];
+ self.widget.call('bus_service', 'trigger', 'notification', [notification]);
+ return 3;
+ }
+ return this._super(...arguments);
+ },
+ });
+ 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('all messages in "Inbox" in "History" after marked all as read', async function (assert) {
+ assert.expect(10);
+
+ const self = this;
+ const messagesData = [];
+ const messageOffset = 200;
+ const partnerOffset = 100;
+ for (let i = messageOffset; i < messageOffset + 40; i++) {
+ messagesData.push({
+ author_id: [partnerOffset + i, 'User ' + (partnerOffset + i)],
+ body: '<p>test ' + i + '</p>',
+ id: i,
+ needaction: true,
+ needaction_partner_ids: [3],
+ });
+ }
+
+ this.data['mail.message'].records = messagesData;
+
+ let messageFetchCount = 0;
+ const initDef = makeTestPromise();
+ const markAllReadDef = makeTestPromise();
+ const clickHistoryDef = makeTestPromise();
+ const loadMoreDef = makeTestPromise();
+
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'mark_all_as_read') {
+ const messageIDs = [];
+ for (let i = 0; i < messagesData.length; i++) {
+ this.data['mail.message'].records[i].history_partner_ids = [3];
+ this.data['mail.message'].records[i].needaction_partner_ids = [];
+ this.data['mail.message'].records[i].needaction = false;
+ messageIDs.push(messageOffset + i);
+ }
+ const notificationData = {
+ type: 'mark_as_read',
+ message_ids: messageIDs,
+ };
+ const notification = [[false, 'res.partner', 3], notificationData];
+ self.widget.call('bus_service', 'trigger', 'notification', [notification]);
+ markAllReadDef.resolve();
+ return 3;
+ }
+ if (args.method === 'message_fetch') {
+ // 1st message_fetch: 'Inbox' initially
+ // 2nd message_fetch: 'History' initially
+ // 3rd message_fetch: 'History' load more
+ assert.step(args.method);
+
+ messageFetchCount++;
+ if (messageFetchCount === 1) {
+ initDef.resolve();
+ }
+ if (messageFetchCount === 2) {
+ clickHistoryDef.resolve();
+ }
+ if (messageFetchCount === 3) {
+ loadMoreDef.resolve();
+ }
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ await initDef;
+ assert.verifySteps(
+ ['message_fetch'],
+ "should fetch messages once for needaction messages (Inbox)"
+ );
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 30,
+ "there should be 30 messages that are loaded in Inbox"
+ );
+
+ await afterNextRender(async () => {
+ const markAllReadButton = document.querySelector('.o_widget_Discuss_controlPanelButtonMarkAllRead');
+ markAllReadButton.click();
+ await markAllReadDef;
+ });
+ assert.containsNone(
+ document.body,
+ '.o_Message',
+ "there should no message in inbox anymore"
+ );
+
+ await afterNextRender(async () => {
+ document.querySelector(`
+ .o_DiscussSidebarItem[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).click();
+ await clickHistoryDef;
+ });
+ assert.verifySteps(
+ ['message_fetch'],
+ "should fetch messages once for history"
+ );
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 30,
+ "there should be 30 messages in History"
+ );
+
+ // simulate a scroll to top to load more messages
+ await afterNextRender(async () => {
+ document.querySelector('.o_MessageList').scrollTop = 0;
+ await loadMoreDef;
+ });
+ assert.verifySteps(
+ ['message_fetch'],
+ "should fetch more messages in history for loadMore"
+ );
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 40,
+ "there should be 40 messages in History"
+ );
+});
+
+QUnit.test('receive new channel message: out of odoo focus (notification, channel)', async function (assert) {
+ assert.expect(4);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ message_unread_counter: 0,
+ name: "General",
+ }],
+ },
+ });
+ 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 = {
+ author_id: [7, "Demo User"],
+ body: "<p>Test</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 126,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ 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 channel message: out of odoo focus (notification, chat)', async function (assert) {
+ assert.expect(4);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_direct_message: [{
+ channel_type: "chat",
+ direct_partner: [{
+ id: 7,
+ name: "Demo User",
+ }],
+ id: 10,
+ message_unread_counter: 0,
+ }],
+ },
+ });
+ 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 = {
+ author_id: [7, "Demo User"],
+ body: "<p>Test</p>",
+ channel_ids: [10],
+ date: "2019-04-20 10:00:00",
+ id: 126,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ 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 channel messages: out of odoo focus (tab title)', async function (assert) {
+ assert.expect(12);
+
+ let step = 0;
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ message_unread_counter: 0,
+ name: "General",
+ }],
+ channel_direct_message: [{
+ channel_type: "chat",
+ direct_partner: [{
+ id: 7,
+ name: "Demo User",
+ }],
+ id: 10,
+ message_unread_counter: 0,
+ }],
+ },
+ });
+ 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 general with odoo focused
+ await afterNextRender(() => {
+ const messageData1 = {
+ author_id: [7, "Demo User"],
+ body: "<p>Test1</p>",
+ channel_ids: [20],
+ date: "2019-04-20 10:00:00",
+ id: 126,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ 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 with odoo focused
+ await afterNextRender(() => {
+ const messageData2 = {
+ author_id: [7, "Demo User"],
+ body: "<p>Test2</p>",
+ channel_ids: [10],
+ date: "2019-04-20 10:00:00",
+ id: 127,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ 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 with odoo focused
+ await afterNextRender(() => {
+ const messageData3 = {
+ author_id: [7, "Demo User"],
+ body: "<p>Test3</p>",
+ channel_ids: [10],
+ date: "2019-04-20 10:00:00",
+ id: 128,
+ message_type: 'comment',
+ model: 'mail.channel',
+ record_name: 'General',
+ res_id: 10,
+ };
+ const notifications3 = [[['my-db', 'mail.channel', 20], 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);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ message_unread_counter: 0,
+ name: "General",
+ }],
+ channel_direct_message: [{
+ channel_type: "chat",
+ direct_partner: [{
+ id: 7,
+ name: "Demo User",
+ }],
+ id: 10,
+ message_unread_counter: 0,
+ }],
+ },
+ });
+ 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"
+ );
+});
+
+});
+});
+});
+
+});