diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/components/discuss/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/discuss/tests')
6 files changed, 7161 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js new file mode 100644 index 00000000..26431207 --- /dev/null +++ b/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js @@ -0,0 +1,408 @@ +odoo.define('mail/static/src/components/discuss/tests/discuss_domain_tests.js', function (require) { +'use strict'; + +const { + afterEach, + afterNextRender, + beforeEach, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('discuss', {}, function () { +QUnit.module('discuss_domain_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { afterEvent, env, widget } = await start(Object.assign({}, params, { + autoOpenDiscuss: true, + data: this.data, + hasDiscuss: true, + })); + this.afterEvent = afterEvent; + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('discuss should filter messages based on given domain', async function (assert) { + assert.expect(2); + + this.data['mail.message'].records.push({ + body: "test", + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + }, { + body: "not empty", + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + }); + await this.start(); + assert.containsN( + document.body, + '.o_Message', + 2, + "should have 2 messages in Inbox initially" + ); + + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + // simulate control panel search + this.env.messaging.discuss.update({ + stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]), + }); + }, + message: "should wait until search filter is applied", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + hint.data.fetchedMessages.length === 1 && + threadViewer.thread.model === 'mail.box' && + threadViewer.thread.id === 'inbox' + ); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "should only have the 1 message containing 'test' remaining after doing a search" + ); +}); + +QUnit.test('discuss should keep filter domain on changing thread', async function (assert) { + assert.expect(3); + + this.data['mail.channel'].records.push({ id: 20 }); + this.data['mail.message'].records.push({ + body: "test", + channel_ids: [20], + }, { + body: "not empty", + channel_ids: [20], + }); + await this.start(); + const channel = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + assert.containsNone( + document.body, + '.o_Message', + "should have no message in Inbox initially" + ); + + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + // simulate control panel search + this.env.messaging.discuss.update({ + stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]), + }); + }, + message: "should wait until search filter is applied", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.box' && + threadViewer.thread.id === 'inbox' + ); + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should have still no message in Inbox after doing a search" + ); + + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${channel.localId}"] + `).click(); + }, + message: "should wait until channel 20 is loaded after clicking on it", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 + ); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "should only have the 1 message containing 'test' in channel 20 (due to the domain still applied on changing thread)" + ); +}); + +QUnit.test('discuss should refresh filtered thread on receiving new message', async function (assert) { + assert.expect(2); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const channel = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${channel.localId}"] + `).click(); + }, + message: "should wait until channel 20 is loaded after clicking on it", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 + ); + }, + }); + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + // simulate control panel search + this.env.messaging.discuss.update({ + stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]), + }); + }, + message: "should wait until search filter is applied", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 + ); + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should have initially no message in channel 20 matching the search 'test'" + ); + + // simulate receiving a message + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => this.env.services.rpc({ + route: '/mail/chat_post', + params: { + message_content: "test", + uuid: channel.uuid, + }, + }), + message: "should wait until channel 20 refreshed its filtered message list", + predicate: data => { + return ( + data.threadViewer.thread.model === 'mail.channel' && + data.threadViewer.thread.id === 20 && + data.hint.type === 'messages-loaded' + ); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "should only have the 1 message containing 'test' in channel 20 after just receiving it" + ); +}); + +QUnit.test('discuss should refresh filtered thread on changing thread', async function (assert) { + assert.expect(4); + + this.data['mail.channel'].records.push({ id: 20 }, { id: 21 }); + await this.start(); + const channel20 = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const channel21 = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 21, + model: 'mail.channel', + }); + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${channel20.localId}"] + `).click(); + }, + message: "should wait until channel 20 is loaded after clicking on it", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 + ); + }, + }); + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + // simulate control panel search + this.env.messaging.discuss.update({ + stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]), + }); + }, + message: "should wait until search filter is applied", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 + ); + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should have initially no message in channel 20 matching the search 'test'" + ); + + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${channel21.localId}"] + `).click(); + }, + message: "should wait until channel 21 is loaded after clicking on it", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 21 + ); + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should have no message in channel 21 matching the search 'test'" + ); + // simulate receiving a message on channel 20 while channel 21 is displayed + await this.env.services.rpc({ + route: '/mail/chat_post', + params: { + message_content: "test", + uuid: channel20.uuid, + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should still have no message in channel 21 matching the search 'test' after receiving a message on channel 20" + ); + + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${channel20.localId}"] + `).click(); + }, + message: "should wait until channel 20 is loaded with the new message after clicking on it", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 && + threadViewer.threadCache.fetchedMessages.length === 1 + ); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "should now have the 1 message containing 'test' in channel 20 when displaying it, after having received the message while the channel was not visible" + ); +}); + +QUnit.test('select all and unselect all buttons should work on filtered thread', async function (assert) { + assert.expect(4); + + this.data['mail.channel'].records.push({ + id: 20, + is_moderator: true, + moderation: true, + name: "general", + }); + this.data['mail.message'].records.push({ + body: "<p>test</p>", + model: 'mail.channel', + moderation_status: 'pending_moderation', + res_id: 20, + }); + await this.start(); + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${this.env.messaging.moderation.localId}"] + `).click(); + }, + message: "should wait until moderation box is loaded after clicking on it", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.box' && + threadViewer.thread.id === 'moderation' + ); + }, + }); + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + // simulate control panel search + this.env.messaging.discuss.update({ + stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]), + }); + }, + message: "should wait until search filter is applied", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.box' && + threadViewer.thread.id === 'moderation' + ); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "should only have the 1 message containing 'test' in moderation box" + ); + assert.notOk( + document.querySelector('.o_Message_checkbox').checked, + "the moderation checkbox should not be checked initially" + ); + + await afterNextRender(() => document.querySelector('.o_widget_Discuss_controlPanelButtonSelectAll').click()); + assert.ok( + document.querySelector('.o_Message_checkbox').checked, + "the moderation checkbox should be checked after clicking on 'select all'" + ); + + await afterNextRender(() => document.querySelector('.o_widget_Discuss_controlPanelButtonUnselectAll').click()); + assert.notOk( + document.querySelector('.o_Message_checkbox').checked, + "the moderation checkbox should be unchecked after clicking on 'unselect all'" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js new file mode 100644 index 00000000..63b524eb --- /dev/null +++ b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js @@ -0,0 +1,725 @@ +odoo.define('mail/static/src/components/discuss/tests/discuss_inbox_tests.js', function (require) { +'use strict'; + +const { + afterEach, + afterNextRender, + beforeEach, + nextAnimationFrame, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const Bus = require('web.Bus'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('discuss', {}, function () { +QUnit.module('discuss_inbox_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + autoOpenDiscuss: true, + data: this.data, + hasDiscuss: true, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('reply: discard on pressing escape', async function (assert) { + assert.expect(9); + + // partner expected to be found by mention + this.data['res.partner'].records.push({ + email: "testpartnert@odoo.com", + id: 11, + name: "TestPartner", + }); + // message expected to be found in inbox + this.data['mail.message'].records.push({ + body: "not empty", + model: 'res.partner', + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + res_id: 20, + }); + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.containsOnce( + document.body, + '.o_Composer', + "should have composer after clicking on reply to message" + ); + + await afterNextRender(() => + document.querySelector(`.o_Composer_buttonEmojis`).click() + ); + assert.containsOnce( + document.body, + '.o_EmojisPopover', + "emojis popover should be opened after click on emojis button" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_Composer_buttonEmojis`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_EmojisPopover', + "emojis popover should be closed after pressing escape on emojis button" + ); + assert.containsOnce( + document.body, + '.o_Composer', + "reply composer should still be opened after pressing escape on emojis button" + ); + + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "@"); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + document.execCommand('insertText', false, "T"); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + document.execCommand('insertText', false, "e"); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + }); + assert.containsOnce( + document.body, + '.o_ComposerSuggestion', + "mention suggestion should be opened after typing @" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_ComposerSuggestion', + "mention suggestion should be closed after pressing escape on mention suggestion" + ); + assert.containsOnce( + document.body, + '.o_Composer', + "reply composer should still be opened after pressing escape on mention suggestion" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_Composer', + "reply composer should be closed after pressing escape if there was no other priority escape handler" + ); +}); + +QUnit.test('reply: discard on discard button click', async function (assert) { + assert.expect(4); + + this.data['mail.message'].records.push({ + body: "not empty", + model: 'res.partner', + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + res_id: 20, + }); + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.containsOnce( + document.body, + '.o_Composer', + "should have composer after clicking on reply to message" + ); + assert.containsOnce( + document.body, + '.o_Composer_buttonDiscard', + "composer should have a discard button" + ); + + await afterNextRender(() => + document.querySelector(`.o_Composer_buttonDiscard`).click() + ); + assert.containsNone( + document.body, + '.o_Composer', + "reply composer should be closed after clicking on discard" + ); +}); + +QUnit.test('reply: discard on reply button toggle', async function (assert) { + assert.expect(3); + + this.data['mail.message'].records.push({ + body: "not empty", + model: 'res.partner', + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + res_id: 20, + }); + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.containsOnce( + document.body, + '.o_Composer', + "should have composer after clicking on reply to message" + ); + + await afterNextRender(() => + document.querySelector(`.o_Message_commandReply`).click() + ); + assert.containsNone( + document.body, + '.o_Composer', + "reply composer should be closed after clicking on reply button again" + ); +}); + +QUnit.test('reply: discard on click away', async function (assert) { + assert.expect(7); + + this.data['mail.message'].records.push({ + body: "not empty", + model: 'res.partner', + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + res_id: 20, + }); + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.containsOnce( + document.body, + '.o_Composer', + "should have composer after clicking on reply to message" + ); + + document.querySelector(`.o_ComposerTextInput_textarea`).click(); + await nextAnimationFrame(); // wait just in case, but nothing is supposed to happen + assert.containsOnce( + document.body, + '.o_Composer', + "reply composer should still be there after clicking inside itself" + ); + + await afterNextRender(() => + document.querySelector(`.o_Composer_buttonEmojis`).click() + ); + assert.containsOnce( + document.body, + '.o_EmojisPopover', + "emojis popover should be opened after clicking on emojis button" + ); + + await afterNextRender(() => { + document.querySelector(`.o_EmojisPopover_emoji`).click(); + }); + assert.containsNone( + document.body, + '.o_EmojisPopover', + "emojis popover should be closed after selecting an emoji" + ); + assert.containsOnce( + document.body, + '.o_Composer', + "reply composer should still be there after selecting an emoji (even though it is technically a click away, it should be considered inside)" + ); + + await afterNextRender(() => + document.querySelector(`.o_Message`).click() + ); + assert.containsNone( + document.body, + '.o_Composer', + "reply composer should be closed after clicking away" + ); +}); + +QUnit.test('"reply to" composer should log note if message replied to is a note', async function (assert) { + assert.expect(6); + + this.data['mail.message'].records.push({ + body: "not empty", + is_discussion: false, + model: 'res.partner', + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + res_id: 20, + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_post') { + assert.step('message_post'); + assert.strictEqual( + args.kwargs.message_type, + "comment", + "should set message type as 'comment'" + ); + assert.strictEqual( + args.kwargs.subtype_xmlid, + "mail.mt_note", + "should set subtype_xmlid as 'note'" + ); + } + return this._super(...arguments); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.strictEqual( + document.querySelector('.o_Composer_buttonSend').textContent.trim(), + "Log", + "Send button text should be 'Log'" + ); + + await afterNextRender(() => + document.execCommand('insertText', false, "Test") + ); + await afterNextRender(() => + document.querySelector('.o_Composer_buttonSend').click() + ); + assert.verifySteps(['message_post']); +}); + +QUnit.test('"reply to" composer should send message if message replied to is not a note', async function (assert) { + assert.expect(6); + + this.data['mail.message'].records.push({ + body: "not empty", + is_discussion: true, + model: 'res.partner', + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + res_id: 20, + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_post') { + assert.step('message_post'); + assert.strictEqual( + args.kwargs.message_type, + "comment", + "should set message type as 'comment'" + ); + assert.strictEqual( + args.kwargs.subtype_xmlid, + "mail.mt_comment", + "should set subtype_xmlid as 'comment'" + ); + } + return this._super(...arguments); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.strictEqual( + document.querySelector('.o_Composer_buttonSend').textContent.trim(), + "Send", + "Send button text should be 'Send'" + ); + + await afterNextRender(() => + document.execCommand('insertText', false, "Test") + ); + await afterNextRender(() => + document.querySelector('.o_Composer_buttonSend').click() + ); + assert.verifySteps(['message_post']); +}); + +QUnit.test('error notifications should not be shown in Inbox', async function (assert) { + assert.expect(3); + + this.data['mail.message'].records.push({ + body: "not empty", + id: 100, + model: 'mail.channel', + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + res_id: 20, + }); + this.data['mail.notification'].records.push({ + mail_message_id: 100, // id of related message + res_partner_id: this.data.currentPartnerId, // must be for current partner + notification_status: 'exception', + notification_type: 'email', + }); + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + assert.containsOnce( + document.body, + '.o_Message_originThreadLink', + "should display origin thread link" + ); + assert.containsNone( + document.body, + '.o_Message_notificationIcon', + "should not display any notification icon in Inbox" + ); +}); + +QUnit.test('show subject of message in Inbox', async function (assert) { + assert.expect(3); + + this.data['mail.message'].records.push({ + body: "not empty", + model: 'mail.channel', // random existing model + needaction: true, // message_fetch domain + needaction_partner_ids: [this.data.currentPartnerId], // not needed, for consistency + subject: "Salutations, voyageur", // will be asserted in the test + }); + await this.start(); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + assert.containsOnce( + document.body, + '.o_Message_subject', + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector('.o_Message_subject').textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); +}); + +QUnit.test('show subject of message in history', async function (assert) { + assert.expect(3); + + this.data['mail.message'].records.push({ + body: "not empty", + history_partner_ids: [3], // not needed, for consistency + model: 'mail.channel', // random existing model + subject: "Salutations, voyageur", // will be asserted in the test + }); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.box_history', + }, + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + assert.containsOnce( + document.body, + '.o_Message_subject', + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector('.o_Message_subject').textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); +}); + +QUnit.test('click on (non-channel/non-partner) origin thread link should redirect to form view', async function (assert) { + assert.expect(9); + + const bus = new Bus(); + bus.on('do-action', null, payload => { + // Callback of doing an action (action manager). + // Expected to be called on click on origin thread link, + // which redirects to form view of record related to origin thread + assert.step('do-action'); + assert.strictEqual( + payload.action.type, + 'ir.actions.act_window', + "action should open a view" + ); + assert.deepEqual( + payload.action.views, + [[false, 'form']], + "action should open form view" + ); + assert.strictEqual( + payload.action.res_model, + 'some.model', + "action should open view with model 'some.model' (model of message origin thread)" + ); + assert.strictEqual( + payload.action.res_id, + 10, + "action should open view with id 10 (id of message origin thread)" + ); + }); + this.data['some.model'] = { fields: {}, records: [{ id: 10 }] }; + this.data['mail.message'].records.push({ + body: "not empty", + model: 'some.model', + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + record_name: "Some record", + res_id: 10, + }); + await this.start({ + env: { + bus, + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "should display a single message" + ); + assert.containsOnce( + document.body, + '.o_Message_originThreadLink', + "should display origin thread link" + ); + assert.strictEqual( + document.querySelector('.o_Message_originThreadLink').textContent, + "Some record", + "origin thread link should display record name" + ); + + document.querySelector('.o_Message_originThreadLink').click(); + assert.verifySteps(['do-action'], "should have made an action on click on origin thread (to open form view)"); +}); + +QUnit.test('subject should not be shown when subject is the same as the thread name', async function (assert) { + assert.expect(1); + + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [100], + model: 'mail.channel', + res_id: 100, + needaction: true, + subject: "Salutations, voyageur", + }); + this.data['mail.channel'].records.push({ + id: 100, + name: "Salutations, voyageur", + }); + await this.start(); + + assert.containsNone( + document.body, + '.o_Message_subject', + "subject should not be shown when subject is the same as the thread name" + ); +}); + +QUnit.test('subject should not be shown when subject is the same as the thread name and both have the same prefix', async function (assert) { + assert.expect(1); + + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [100], + model: 'mail.channel', + res_id: 100, + needaction: true, + subject: "Re: Salutations, voyageur", + }); + this.data['mail.channel'].records.push({ + id: 100, + name: "Re: Salutations, voyageur", + }); + await this.start(); + + assert.containsNone( + document.body, + '.o_Message_subject', + "subject should not be shown when subject is the same as the thread name and both have the same prefix" + ); +}); + +QUnit.test('subject should not be shown when subject differs from thread name only by the "Re:" prefix', async function (assert) { + assert.expect(1); + + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [100], + model: 'mail.channel', + res_id: 100, + needaction: true, + subject: "Re: Salutations, voyageur", + }); + this.data['mail.channel'].records.push({ + id: 100, + name: "Salutations, voyageur", + }); + await this.start(); + + assert.containsNone( + document.body, + '.o_Message_subject', + "should not display subject when subject differs from thread name only by the 'Re:' prefix" + ); +}); + +QUnit.test('subject should not be shown when subject differs from thread name only by the "Fw:" and "Re:" prefix', async function (assert) { + assert.expect(1); + + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [100], + model: 'mail.channel', + res_id: 100, + needaction: true, + subject: "Fw: Re: Salutations, voyageur", + }); + this.data['mail.channel'].records.push({ + id: 100, + name: "Salutations, voyageur", + }); + await this.start(); + + assert.containsNone( + document.body, + '.o_Message_subject', + "should not display subject when subject differs from thread name only by the 'Fw:' and Re:' prefix" + ); +}); + +QUnit.test('subject should be shown when the thread name has an extra prefix compared to subject', async function (assert) { + assert.expect(1); + + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [100], + model: 'mail.channel', + res_id: 100, + needaction: true, + subject: "Salutations, voyageur", + }); + this.data['mail.channel'].records.push({ + id: 100, + name: "Re: Salutations, voyageur", + }); + await this.start(); + + assert.containsOnce( + document.body, + '.o_Message_subject', + "subject should be shown when the thread name has an extra prefix compared to subject" + ); +}); + +QUnit.test('subject should not be shown when subject differs from thread name only by the "fw:" prefix and both contain another common prefix', async function (assert) { + assert.expect(1); + + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [100], + model: 'mail.channel', + res_id: 100, + needaction: true, + subject: "fw: re: Salutations, voyageur", + }); + this.data['mail.channel'].records.push({ + id: 100, + name: "Re: Salutations, voyageur", + }); + await this.start(); + + assert.containsNone( + document.body, + '.o_Message_subject', + "subject should not be shown when subject differs from thread name only by the 'fw:' prefix and both contain another common prefix" + ); +}); + +QUnit.test('subject should not be shown when subject differs from thread name only by the "Re: Re:" prefix', async function (assert) { + assert.expect(1); + + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [100], + model: 'mail.channel', + res_id: 100, + needaction: true, + subject: "Re: Re: Salutations, voyageur", + }); + this.data['mail.channel'].records.push({ + id: 100, + name: "Salutations, voyageur", + }); + await this.start(); + + assert.containsNone( + document.body, + '.o_Message_subject', + "should not display subject when subject differs from thread name only by the 'Re: Re:'' prefix" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js new file mode 100644 index 00000000..c4c53cb5 --- /dev/null +++ b/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js @@ -0,0 +1,1180 @@ +odoo.define('mail/static/src/components/discuss/tests/discuss_moderation_tests.js', function (require) { +'use strict'; + +const { + afterEach, + afterNextRender, + beforeEach, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('discuss', {}, function () { +QUnit.module('discuss_moderation_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + autoOpenDiscuss: true, + data: this.data, + hasDiscuss: true, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('as moderator, moderated channel with pending moderation message', async function (assert) { + assert.expect(37); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + is_moderator: true, // current user is expected to be moderator of channel + moderation: true, // for consistency, but not used in the scope of this test + name: "general", // random name, will be asserted in the test + }); + this.data['mail.message'].records.push({ + body: "<p>test</p>", // random body, will be asserted in the test + model: 'mail.channel', // expected value to link message to channel + moderation_status: 'pending_moderation', // message is expected to be pending moderation + res_id: 20, // id of the channel + }); + await this.start(); + + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.moderation.localId + }"] + `), + "should display the moderation box in the sidebar" + ); + const mailboxCounter = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.moderation.localId + }"] + .o_DiscussSidebarItem_counter + `); + assert.ok( + mailboxCounter, + "there should be a counter next to the moderation mailbox in the sidebar" + ); + assert.strictEqual( + mailboxCounter.textContent.trim(), + "1", + "the mailbox counter of the moderation mailbox should display '1'" + ); + + // 1. go to moderation mailbox + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.moderation.localId + }"] + `).click() + ); + // check message + assert.containsOnce( + document.body, + '.o_Message', + "should be only one message in moderation box" + ); + assert.strictEqual( + document.querySelector('.o_Message_content').textContent, + "test", + "this message pending moderation should have the correct content" + ); + assert.containsOnce( + document.body, + '.o_Message_originThreadLink', + "thee message should have one origin" + ); + assert.strictEqual( + document.querySelector('.o_Message_originThreadLink').textContent, + "#general", + "the message pending moderation should have correct origin as its linked document" + ); + assert.containsOnce( + document.body, + '.o_Message_checkbox', + "there should be a moderation checkbox next to the message" + ); + assert.notOk( + document.querySelector('.o_Message_checkbox').checked, + "the moderation checkbox should be unchecked by default" + ); + // check select all (enabled) / unselect all (disabled) buttons + assert.containsOnce( + document.body, + '.o_widget_Discuss_controlPanelButtonSelectAll', + "there should be a 'Select All' button in the control panel" + ); + assert.doesNotHaveClass( + document.querySelector('.o_widget_Discuss_controlPanelButtonSelectAll'), + 'disabled', + "the 'Select All' button should not be disabled" + ); + assert.containsOnce( + document.body, + '.o_widget_Discuss_controlPanelButtonUnselectAll', + "there should be a 'Unselect All' button in the control panel" + ); + assert.hasClass( + document.querySelector('.o_widget_Discuss_controlPanelButtonUnselectAll'), + 'disabled', + "the 'Unselect All' button should be disabled" + ); + // check moderate all buttons (invisible) + assert.containsN( + document.body, + '.o_widget_Discuss_controlPanelButtonModeration', + 3, + "there should be 3 buttons to moderate selected messages in the control panel" + ); + assert.containsOnce( + document.body, + '.o_widget_Discuss_controlPanelButtonModeration.o-accept', + "there should one moderate button to accept messages pending moderation" + ); + assert.isNotVisible( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'), + "the moderate button 'Accept' should be invisible by default" + ); + assert.containsOnce( + document.body, + '.o_widget_Discuss_controlPanelButtonModeration.o-reject', + "there should one moderate button to reject messages pending moderation" + ); + assert.isNotVisible( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-reject'), + "the moderate button 'Reject' should be invisible by default" + ); + assert.containsOnce( + document.body, + '.o_widget_Discuss_controlPanelButtonModeration.o-discard', + "there should one moderate button to discard messages pending moderation" + ); + assert.isNotVisible( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-discard'), + "the moderate button 'Discard' should be invisible by default" + ); + + // click on message moderation checkbox + await afterNextRender(() => document.querySelector('.o_Message_checkbox').click()); + assert.ok( + document.querySelector('.o_Message_checkbox').checked, + "the moderation checkbox should become checked after click" + ); + // check select all (disabled) / unselect all buttons (enabled) + assert.hasClass( + document.querySelector('.o_widget_Discuss_controlPanelButtonSelectAll'), + 'disabled', + "the 'Select All' button should be disabled" + ); + assert.doesNotHaveClass( + document.querySelector('.o_widget_Discuss_controlPanelButtonUnselectAll'), + 'disabled', + "the 'Unselect All' button should not be disabled" + ); + // check moderate all buttons updated (visible) + assert.isVisible( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'), + "the moderate button 'Accept' should be visible" + ); + assert.isVisible( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-reject'), + "the moderate button 'Reject' should be visible" + ); + assert.isVisible( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-discard'), + "the moderate button 'Discard' should be visible" + ); + + // test select buttons + await afterNextRender(() => + document.querySelector('.o_widget_Discuss_controlPanelButtonUnselectAll').click() + ); + assert.notOk( + document.querySelector('.o_Message_checkbox').checked, + "the moderation checkbox should become unchecked after click" + ); + + await afterNextRender(() => + document.querySelector('.o_widget_Discuss_controlPanelButtonSelectAll').click() + ); + assert.ok( + document.querySelector('.o_Message_checkbox').checked, + "the moderation checkbox should become checked again after click" + ); + + // 2. go to channel 'general' + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `).click() + ); + // check correct message + assert.containsOnce( + document.body, + '.o_Message', + "should be only one message in general channel" + ); + assert.containsOnce( + document.body, + '.o_Message_checkbox', + "there should be a moderation checkbox next to the message" + ); + assert.notOk( + document.querySelector('.o_Message_checkbox').checked, + "the moderation checkbox should not be checked here" + ); + await afterNextRender(() => document.querySelector('.o_Message_checkbox').click()); + // Don't test moderation actions visibility, since it is similar to moderation box. + + // 3. test discard button + await afterNextRender(() => + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-discard').click() + ); + assert.containsOnce( + document.body, + '.o_ModerationDiscardDialog', + "discard dialog should be open" + ); + // the dialog will be tested separately + await afterNextRender(() => + document.querySelector('.o_ModerationDiscardDialog .o-cancel').click() + ); + assert.containsNone( + document.body, + '.o_ModerationDiscardDialog', + "discard dialog should be closed" + ); + + // 4. test reject button + await afterNextRender(() => + document.querySelector(` + .o_widget_Discuss_controlPanelButtonModeration.o-reject + `).click() + ); + assert.containsOnce( + document.body, + '.o_ModerationRejectDialog', + "reject dialog should be open" + ); + // the dialog will be tested separately + await afterNextRender(() => + document.querySelector('.o_ModerationRejectDialog .o-cancel').click() + ); + assert.containsNone( + document.body, + '.o_ModerationRejectDialog', + "reject dialog should be closed" + ); + + // 5. test accept button + await afterNextRender(() => + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept').click() + ); + assert.containsOnce( + document.body, + '.o_Message', + "should still be only one message in general channel" + ); + assert.containsNone( + document.body, + '.o_Message_checkbox', + "there should not be a moderation checkbox next to the message" + ); +}); + +QUnit.test('as moderator, accept pending moderation message', async function (assert) { + assert.expect(12); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + is_moderator: true, // current user is expected to be moderator of channel + moderation: true, // for consistency, but not used in the scope of this test + name: "general", // random name, will be asserted in the test + }); + this.data['mail.message'].records.push({ + body: "<p>test</p>", // random body, will be asserted in the test + id: 100, // random unique id, will be asserted during the test + model: 'mail.channel', // expected value to link message to channel + moderation_status: 'pending_moderation', // message is expected to be pending moderation + res_id: 20, // id of the channel + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'moderate') { + assert.step('moderate'); + const messageIDs = args.args[0]; + const decision = args.args[1]; + assert.strictEqual( + messageIDs.length, + 1, + "should moderate one message" + ); + assert.strictEqual( + messageIDs[0], + 100, + "should moderate message with ID 100" + ); + assert.strictEqual( + decision, + 'accept', + "should accept the message" + ); + } + return this._super(...arguments); + }, + }); + + // 1. go to moderation box + const moderationBox = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.moderation.localId + }"] + `); + assert.ok( + moderationBox, + "should display the moderation box" + ); + + await afterNextRender(() => moderationBox.click()); + assert.ok( + document.querySelector(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `), + "should display the message to moderate" + ); + const acceptButton = document.querySelector(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + .o_Message_moderationAction.o-accept + `); + assert.ok(acceptButton, "should display the accept button"); + + await afterNextRender(() => acceptButton.click()); + assert.verifySteps(['moderate']); + assert.containsOnce( + document.body, + '.o_MessageList_emptyTitle', + "should now have no message displayed in moderation box" + ); + + // 2. go to channel 'general' + const channel = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `); + assert.ok( + channel, + "should display the general channel" + ); + + await afterNextRender(() => channel.click()); + const message = document.querySelector(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `); + assert.ok( + message, + "should display the accepted message" + ); + assert.containsNone( + message, + '.o_Message_moderationPending', + "the message should not be pending moderation" + ); +}); + +QUnit.test('as moderator, reject pending moderation message (reject with explanation)', async function (assert) { + assert.expect(23); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + is_moderator: true, // current user is expected to be moderator of channel + moderation: true, // for consistency, but not used in the scope of this test + name: "general", // random name, will be asserted in the test + }); + this.data['mail.message'].records.push({ + body: "<p>test</p>", // random body, will be asserted in the test + id: 100, // random unique id, will be asserted during the test + model: 'mail.channel', // expected value to link message to channel + moderation_status: 'pending_moderation', // message is expected to be pending moderation + res_id: 20, // id of the channel + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'moderate') { + assert.step('moderate'); + const messageIDs = args.args[0]; + const decision = args.args[1]; + const kwargs = args.kwargs; + assert.strictEqual( + messageIDs.length, + 1, + "should moderate one message" + ); + assert.strictEqual( + messageIDs[0], + 100, + "should moderate message with ID 100" + ); + assert.strictEqual( + decision, + 'reject', + "should reject the message" + ); + assert.strictEqual( + kwargs.title, + "Message Rejected", + "should have correct reject message title" + ); + assert.strictEqual( + kwargs.comment, + "Your message was rejected by moderator.", + "should have correct reject message body / comment" + ); + } + return this._super(...arguments); + }, + }); + + // 1. go to moderation box + const moderationBox = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.moderation.localId + }"] + `); + assert.ok( + moderationBox, + "should display the moderation box" + ); + + await afterNextRender(() => moderationBox.click()); + const pendingMessage = document.querySelector(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `); + assert.ok( + pendingMessage, + "should display the message to moderate" + ); + const rejectButton = pendingMessage.querySelector(':scope .o_Message_moderationAction.o-reject'); + assert.ok( + rejectButton, + "should display the reject button" + ); + + await afterNextRender(() => rejectButton.click()); + const dialog = document.querySelector('.o_ModerationRejectDialog'); + assert.ok( + dialog, + "a dialog should be prompt to the moderator on click reject" + ); + assert.strictEqual( + dialog.querySelector('.modal-title').textContent, + "Send explanation to author", + "dialog should have correct title" + ); + + const messageTitle = dialog.querySelector(':scope .o_ModerationRejectDialog_title'); + assert.ok( + messageTitle, + "should have a title for rejecting" + ); + assert.hasAttrValue( + messageTitle, + 'placeholder', + "Subject", + "title for reject reason should have correct placeholder" + ); + assert.strictEqual( + messageTitle.value, + "Message Rejected", + "title for reject reason should have correct default value" + ); + + const messageComment = dialog.querySelector(':scope .o_ModerationRejectDialog_comment'); + assert.ok( + messageComment, + "should have a comment for rejecting" + ); + assert.hasAttrValue( + messageComment, + 'placeholder', + "Mail Body", + "comment for reject reason should have correct placeholder" + ); + assert.strictEqual( + messageComment.value, + "Your message was rejected by moderator.", + "comment for reject reason should have correct default text content" + ); + const confirmReject = dialog.querySelector(':scope .o-reject'); + assert.ok( + confirmReject, + "should have reject button" + ); + assert.strictEqual( + confirmReject.textContent, + "Reject" + ); + + await afterNextRender(() => confirmReject.click()); + assert.verifySteps(['moderate']); + assert.containsOnce( + document.body, + '.o_MessageList_emptyTitle', + "should now have no message displayed in moderation box" + ); + + // 2. go to channel 'general' + const channel = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `); + assert.ok( + channel, + 'should display the general channel' + ); + + await afterNextRender(() => channel.click()); + assert.containsNone( + document.body, + '.o_Message', + "should now have no message in channel" + ); +}); + +QUnit.test('as moderator, discard pending moderation message (reject without explanation)', async function (assert) { + assert.expect(16); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + is_moderator: true, // current user is expected to be moderator of channel + moderation: true, // for consistency, but not used in the scope of this test + name: "general", // random name, will be asserted in the test + }); + this.data['mail.message'].records.push({ + body: "<p>test</p>", // random body, will be asserted in the test + id: 100, // random unique id, will be asserted during the test + model: 'mail.channel', // expected value to link message to channel + moderation_status: 'pending_moderation', // message is expected to be pending moderation + res_id: 20, // id of the channel + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'moderate') { + assert.step('moderate'); + const messageIDs = args.args[0]; + const decision = args.args[1]; + assert.strictEqual(messageIDs.length, 1, "should moderate one message"); + assert.strictEqual(messageIDs[0], 100, "should moderate message with ID 100"); + assert.strictEqual(decision, 'discard', "should discard the message"); + } + return this._super(...arguments); + }, + }); + + // 1. go to moderation box + const moderationBox = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.moderation.localId + }"] + `); + assert.ok( + moderationBox, + "should display the moderation box" + ); + + await afterNextRender(() => moderationBox.click()); + const pendingMessage = document.querySelector(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `); + assert.ok( + pendingMessage, + "should display the message to moderate" + ); + + const discardButton = pendingMessage.querySelector(` + :scope .o_Message_moderationAction.o-discard + `); + assert.ok( + discardButton, + "should display the discard button" + ); + + await afterNextRender(() => discardButton.click()); + const dialog = document.querySelector('.o_ModerationDiscardDialog'); + assert.ok( + dialog, + "a dialog should be prompt to the moderator on click discard" + ); + assert.strictEqual( + dialog.querySelector('.modal-title').textContent, + "Confirmation", + "dialog should have correct title" + ); + assert.strictEqual( + dialog.textContent, + "Confirmation×You are going to discard 1 message.Do you confirm the action?DiscardCancel", + "should warn the user on discard action" + ); + + const confirmDiscard = dialog.querySelector(':scope .o-discard'); + assert.ok( + confirmDiscard, + "should have discard button" + ); + assert.strictEqual( + confirmDiscard.textContent, + "Discard" + ); + + await afterNextRender(() => confirmDiscard.click()); + assert.verifySteps(['moderate']); + assert.containsOnce( + document.body, + '.o_MessageList_emptyTitle', + "should now have no message displayed in moderation box" + ); + + // 2. go to channel 'general' + const channel = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `); + assert.ok( + channel, + "should display the general channel" + ); + + await afterNextRender(() => channel.click()); + assert.containsNone( + document.body, + '.o_Message', + "should now have no message in channel" + ); +}); + +QUnit.test('as author, send message in moderated channel', async function (assert) { + assert.expect(4); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + moderation: true, // channel must be moderated to test the feature + name: "general", // random name, will be asserted in the test + }); + await this.start(); + const channel = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `); + assert.ok( + channel, + "should display the general channel" + ); + + // go to channel 'general' + await afterNextRender(() => channel.click()); + assert.containsNone( + document.body, + '.o_Message', + "should have no message in channel" + ); + + // post a message + await afterNextRender(() => { + const textInput = document.querySelector('.o_ComposerTextInput_textarea'); + textInput.focus(); + document.execCommand('insertText', false, "Some Text"); + }); + await afterNextRender(() => document.querySelector('.o_Composer_buttonSend').click()); + const messagePending = document.querySelector('.o_Message_moderationPending'); + assert.ok( + messagePending, + "should display the pending message with pending info" + ); + assert.hasClass( + messagePending, + 'o-author', + "the message should be pending moderation as author" + ); +}); + +QUnit.test('as author, sent message accepted in moderated channel', async function (assert) { + assert.expect(5); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + moderation: true, // for consistency, but not used in the scope of this test + name: "general", // random name, will be asserted in the test + }); + this.data['mail.message'].records.push({ + body: "not empty", + id: 100, // random unique id, will be referenced in the test + model: 'mail.channel', // expected value to link message to channel + moderation_status: 'pending_moderation', // message is expected to be pending + res_id: 20, // id of the channel + }); + await this.start(); + + const channel = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `); + assert.ok( + channel, + "should display the general channel" + ); + + await afterNextRender(() => channel.click()); + const messagePending = document.querySelector(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + .o_Message_moderationPending + `); + assert.ok( + messagePending, + "should display the pending message with pending info" + ); + assert.hasClass( + messagePending, + 'o-author', + "the message should be pending moderation as author" + ); + + // simulate accepted message + await afterNextRender(() => { + const messageData = { + id: 100, + moderation_status: 'accepted', + }; + const notification = [[false, 'mail.channel', 20], messageData]; + this.widget.call('bus_service', 'trigger', 'notification', [notification]); + }); + + // check message is accepted + const message = document.querySelector(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `); + assert.ok( + message, + "should still display the message" + ); + assert.containsNone( + message, + '.o_Message_moderationPending', + "the message should not be in pending moderation anymore" + ); +}); + +QUnit.test('as author, sent message rejected in moderated channel', async function (assert) { + assert.expect(4); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + moderation: true, // for consistency, but not used in the scope of this test + name: "general", // random name, will be asserted in the test + }); + this.data['mail.message'].records.push({ + body: "not empty", + id: 100, // random unique id, will be referenced in the test + model: 'mail.channel', // expected value to link message to channel + moderation_status: 'pending_moderation', // message is expected to be pending + res_id: 20, // id of the channel + }); + await this.start(); + + const channel = document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `); + assert.ok( + channel, + "should display the general channel" + ); + + await afterNextRender(() => channel.click()); + const messagePending = document.querySelector(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + .o_Message_moderationPending + `); + assert.ok( + messagePending, + "should display the pending message with pending info" + ); + assert.hasClass( + messagePending, + 'o-author', + "the message should be pending moderation as author" + ); + + // simulate reject from moderator + await afterNextRender(() => { + const notifData = { + type: 'deletion', + message_ids: [100], + }; + const notification = [[false, 'res.partner', this.env.messaging.currentPartner.id], notifData]; + this.widget.call('bus_service', 'trigger', 'notification', [notification]); + }); + // check no message + assert.containsNone( + document.body, + '.o_Message', + "message should be removed from channel after reject" + ); +}); + +QUnit.test('as moderator, pending moderation message accessibility', async function (assert) { + // pending moderation message should appear in moderation box and in origin thread + assert.expect(3); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + is_moderator: true, // current user is expected to be moderator of channel + moderation: true, // channel must be moderated to test the feature + }); + this.data['mail.message'].records.push({ + body: "not empty", + id: 100, // random unique id, will be referenced in the test + model: 'mail.channel', // expected value to link message to channel + moderation_status: 'pending_moderation', // message is expected to be pending + res_id: 20, // id of the channel + }); + await this.start(); + + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ id: 20, model: 'mail.channel' }); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.moderation.localId + }"] + `), + "should display the moderation box in the sidebar" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${thread.localId}"] + `).click() + ); + const message = this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }); + assert.containsOnce( + document.body, + `.o_Message[data-message-local-id="${message.localId}"]`, + "the pending moderation message should be in the channel" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.moderation.localId + }"] + `).click() + ); + assert.containsOnce( + document.body, + `.o_Message[data-message-local-id="${message.localId}"]`, + "the pending moderation message should be in moderation box" + ); +}); + +QUnit.test('as author, pending moderation message should appear in origin thread', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + moderation: true, // channel must be moderated to test the feature + }); + this.data['mail.message'].records.push({ + author_id: this.data.currentPartnerId, // test as author of message + body: "not empty", + id: 100, // random unique id, will be referenced in the test + model: 'mail.channel', // expected value to link message to channel + moderation_status: 'pending_moderation', // message is expected to be pending + res_id: 20, // id of the channel + }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ id: 20, model: 'mail.channel' }); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${thread.localId}"] + `).click() + ); + const message = this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }); + assert.containsOnce( + document.body, + `.o_Message[data-message-local-id="${message.localId}"]`, + "the pending moderation message should be in the channel" + ); +}); + +QUnit.test('as moderator, new pending moderation message posted by someone else', async function (assert) { + // the message should appear in origin thread and moderation box if I moderate it + assert.expect(3); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + is_moderator: true, // current user is expected to be moderator of channel + moderation: true, // channel must be moderated to test the feature + }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ id: 20, model: 'mail.channel' }); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${thread.localId}"] + `).click() + ); + assert.containsNone( + document.body, + `.o_Message`, + "should have no message in the channel initially" + ); + + // simulate receiving the message + const messageData = { + author_id: [10, 'john doe'], // random id, different than current partner + body: "not empty", + channel_ids: [], // server do NOT return channel_id of the message if pending moderation + id: 1, // random unique id + model: 'mail.channel', // expected value to link message to channel + moderation_status: 'pending_moderation', // message is expected to be pending + res_id: 20, // id of the channel + }; + await afterNextRender(() => { + const notifications = [[ + ['my-db', 'res.partner', this.env.messaging.currentPartner.id], + { type: 'moderator', message: messageData }, + ]]; + this.widget.call('bus_service', 'trigger', 'notification', notifications); + }); + const message = this.env.models['mail.message'].findFromIdentifyingData({ id: 1 }); + assert.containsOnce( + document.body, + `.o_Message[data-message-local-id="${message.localId}"]`, + "the pending moderation message should be in the channel" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.moderation.localId + }"] + `).click() + ); + assert.containsOnce( + document.body, + `.o_Message[data-message-local-id="${message.localId}"]`, + "the pending moderation message should be in moderation box" + ); +}); + +QUnit.test('accept multiple moderation messages', async function (assert) { + assert.expect(5); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + is_moderator: true, // current user is expected to be moderator of channel + moderation: true, // channel must be moderated to test the feature + }); + this.data['mail.message'].records.push( + { + body: "not empty", + model: 'mail.channel', + moderation_status: 'pending_moderation', + res_id: 20, + }, + { + body: "not empty", + model: 'mail.channel', + moderation_status: 'pending_moderation', + res_id: 20, + }, + { + body: "not empty", + model: 'mail.channel', + moderation_status: 'pending_moderation', + res_id: 20, + } + ); + + await this.start({ + discuss: { + params: { + default_active_id: 'mail.box_moderation', + }, + }, + }); + + assert.containsN( + document.body, + '.o_Message', + 3, + "should initially display 3 messages" + ); + + await afterNextRender(() => { + document.querySelectorAll('.o_Message_checkbox')[0].click(); + document.querySelectorAll('.o_Message_checkbox')[1].click(); + }); + assert.containsN( + document.body, + '.o_Message_checkbox:checked', + 2, + "2 messages should have been checked after clicking on their respective checkbox" + ); + assert.doesNotHaveClass( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'), + 'o_hidden', + "global accept button should be displayed as two messages are selected" + ); + + await afterNextRender(() => + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept').click() + ); + assert.containsN( + document.body, + '.o_Message', + 1, + "should display 1 message as the 2 others have been accepted" + ); + assert.hasClass( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'), + 'o_hidden', + "global accept button should no longer be displayed as messages have been unselected" + ); +}); + +QUnit.test('accept multiple moderation messages after having accepted other messages', async function (assert) { + assert.expect(5); + + this.data['mail.channel'].records.push({ + id: 20, // random unique id, will be used to link message and will be referenced in the test + is_moderator: true, // current user is expected to be moderator of channel + moderation: true, // channel must be moderated to test the feature + }); + this.data['mail.message'].records.push( + { + body: "not empty", + model: 'mail.channel', + moderation_status: 'pending_moderation', + res_id: 20, + }, + { + body: "not empty", + model: 'mail.channel', + moderation_status: 'pending_moderation', + res_id: 20, + }, + { + body: "not empty", + model: 'mail.channel', + moderation_status: 'pending_moderation', + res_id: 20, + } + ); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.box_moderation', + }, + }, + }); + assert.containsN( + document.body, + '.o_Message', + 3, + "should initially display 3 messages" + ); + + await afterNextRender(() => { + document.querySelectorAll('.o_Message_checkbox')[0].click(); + }); + await afterNextRender(() => + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept').click() + ); + await afterNextRender(() => document.querySelectorAll('.o_Message_checkbox')[0].click()); + assert.containsOnce( + document.body, + '.o_Message_checkbox:checked', + "a message should have been checked after clicking on its checkbox" + ); + assert.doesNotHaveClass( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'), + 'o_hidden', + "global accept button should be displayed as a message is selected" + ); + + await afterNextRender(() => + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept').click() + ); + assert.containsOnce( + document.body, + '.o_Message', + "should display only one message left after the two others has been accepted" + ); + assert.hasClass( + document.querySelector('.o_widget_Discuss_controlPanelButtonModeration.o-accept'), + 'o_hidden', + "global accept button should no longer be displayed as message has been unselected" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js new file mode 100644 index 00000000..a24ce411 --- /dev/null +++ b/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js @@ -0,0 +1,238 @@ +odoo.define('mail/static/src/components/discuss/tests/discuss_pinned_tests.js', function (require) { +'use strict'; + +const { + afterEach, + afterNextRender, + beforeEach, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('discuss', {}, function () { +QUnit.module('discuss_pinned_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + autoOpenDiscuss: true, + data: this.data, + hasDiscuss: true, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('sidebar: pinned channel 1: init with one pinned channel', async function (assert) { + assert.expect(2); + + // channel that is expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + assert.containsOnce( + document.body, + `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`, + "The Inbox is opened in discuss" + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarItem[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"]`, + "should have the only channel of which user is member in discuss sidebar" + ); +}); + +QUnit.test('sidebar: pinned channel 2: open pinned channel', async function (assert) { + assert.expect(1); + + // channel that is expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + + const threadGeneral = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebarItem[data-thread-local-id="${ + threadGeneral.localId + }"]`).click() + ); + assert.containsOnce( + document.body, + `.o_Discuss_thread[data-thread-local-id="${threadGeneral.localId}"]`, + "The channel #General is displayed in discuss" + ); +}); + +QUnit.test('sidebar: pinned channel 3: open pinned channel and unpin it', async function (assert) { + assert.expect(8); + + // channel that is expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ + id: 20, + is_minimized: true, + state: 'open', + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'execute_command') { + assert.step('execute_command'); + assert.deepEqual(args.args[0], [20], + "The right id is sent to the server to remove" + ); + assert.strictEqual(args.kwargs.command, 'leave', + "The right command is sent to the server" + ); + } + if (args.method === 'channel_fold') { + assert.step('channel_fold'); + } + return this._super(...arguments); + }, + }); + + const threadGeneral = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebarItem[data-thread-local-id="${ + threadGeneral.localId + }"]`).click() + ); + assert.verifySteps([], "neither channel_fold nor execute_command are called yet"); + await afterNextRender(() => + document.querySelector('.o_DiscussSidebarItem_commandLeave').click() + ); + assert.verifySteps( + [ + 'channel_fold', + 'execute_command' + ], + "both channel_fold and execute_command have been called when unpinning a channel" + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`, + "The channel must have been removed from discuss sidebar" + ); + assert.containsOnce( + document.body, + '.o_Discuss_noThread', + "should have no thread opened in discuss" + ); +}); + +QUnit.test('sidebar: unpin channel from bus', async function (assert) { + assert.expect(5); + + // channel that is expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const threadGeneral = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + + assert.containsOnce( + document.body, + `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`, + "The Inbox is opened in discuss" + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`, + "1 channel is present in discuss sidebar and it is 'general'" + ); + + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebarItem[data-thread-local-id="${ + threadGeneral.localId + }"]`).click() + ); + assert.containsOnce( + document.body, + `.o_Discuss_thread[data-thread-local-id="${threadGeneral.localId}"]`, + "The channel #General is opened in discuss" + ); + + // Simulate receiving a leave channel notification + // (e.g. from user interaction from another device or browser tab) + await afterNextRender(() => { + const notif = [ + ["dbName", 'res.partner', this.env.messaging.currentPartner.id], + { + channel_type: 'channel', + id: 20, + info: 'unsubscribe', + name: "General", + public: 'public', + state: 'open', + } + ]; + this.env.services.bus_service.trigger('notification', [notif]); + }); + assert.containsOnce( + document.body, + '.o_Discuss_noThread', + "should have no thread opened in discuss" + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`, + "The channel must have been removed from discuss sidebar" + ); +}); + +QUnit.test('[technical] sidebar: channel group_based_subscription: mandatorily pinned', async function (assert) { + assert.expect(2); + + // FIXME: The following is admittedly odd. + // Fixing it should entail a deeper reflexion on the group_based_subscription + // and is_pinned functionalities, especially in python. + // task-2284357 + + // channel that is expected to be found in the sidebar + this.data['mail.channel'].records.push({ + group_based_subscription: true, // expected value for this test + id: 20, // random unique id, will be referenced in the test + is_pinned: false, // expected value for this test + }); + await this.start(); + const threadGeneral = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + assert.containsOnce( + document.body, + `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`, + "The channel #General is in discuss sidebar" + ); + assert.containsNone( + document.body, + 'o_DiscussSidebarItem_commandLeave', + "The group_based_subscription channel is not unpinnable" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/discuss/tests/discuss_sidebar_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_sidebar_tests.js new file mode 100644 index 00000000..34c884eb --- /dev/null +++ b/addons/mail/static/src/components/discuss/tests/discuss_sidebar_tests.js @@ -0,0 +1,163 @@ +odoo.define('mail/static/src/components/discuss/tests/discuss_sidebar_tests.js', function (require) { +'use strict'; + +const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js'); +const { + afterEach, + afterNextRender, + beforeEach, + nextAnimationFrame, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('discuss', {}, function () { +QUnit.module('discuss_sidebar_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + autoOpenDiscuss: true, + data: this.data, + hasDiscuss: true, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('sidebar find shows channels matching search term', async function (assert) { + assert.expect(3); + + this.data['mail.channel'].records.push({ + channel_partner_ids: [], + channel_type: 'channel', + id: 20, + members: [], + name: 'test', + public: 'public', + }); + const searchReadDef = makeDeferred(); + await this.start({ + async mockRPC(route, args) { + const res = await this._super(...arguments); + if (args.method === 'search_read') { + searchReadDef.resolve(); + } + return res; + }, + }); + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebar_groupHeaderItemAdd`).click() + ); + document.querySelector(`.o_DiscussSidebar_itemNew`).focus(); + document.execCommand('insertText', false, "test"); + document.querySelector(`.o_DiscussSidebar_itemNew`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_DiscussSidebar_itemNew`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + + await searchReadDef; + await nextAnimationFrame(); // ensures search_read rpc is rendered. + const results = document.querySelectorAll('.ui-autocomplete .ui-menu-item a'); + assert.ok( + results, + "should have autocomplete suggestion after typing on 'find or create channel' input" + ); + assert.strictEqual( + results.length, + // When searching for a single existing channel, the results list will have at least 3 lines: + // One for the existing channel itself + // One for creating a public channel with the search term + // One for creating a private channel with the search term + 3 + ); + assert.strictEqual( + results[0].textContent, + "test", + "autocomplete suggestion should target the channel matching search term" + ); +}); + +QUnit.test('sidebar find shows channels matching search term even when user is member', async function (assert) { + assert.expect(3); + + this.data['mail.channel'].records.push({ + channel_partner_ids: [this.data.currentPartnerId], + channel_type: 'channel', + id: 20, + members: [this.data.currentPartnerId], + name: 'test', + public: 'public', + }); + const searchReadDef = makeDeferred(); + await this.start({ + async mockRPC(route, args) { + const res = await this._super(...arguments); + if (args.method === 'search_read') { + searchReadDef.resolve(); + } + return res; + }, + }); + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebar_groupHeaderItemAdd`).click() + ); + document.querySelector(`.o_DiscussSidebar_itemNew`).focus(); + document.execCommand('insertText', false, "test"); + document.querySelector(`.o_DiscussSidebar_itemNew`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_DiscussSidebar_itemNew`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + + await searchReadDef; + await nextAnimationFrame(); + const results = document.querySelectorAll('.ui-autocomplete .ui-menu-item a'); + assert.ok( + results, + "should have autocomplete suggestion after typing on 'find or create channel' input" + ); + assert.strictEqual( + results.length, + // When searching for a single existing channel, the results list will have at least 3 lines: + // One for the existing channel itself + // One for creating a public channel with the search term + // One for creating a private channel with the search term + 3 + ); + assert.strictEqual( + results[0].textContent, + "test", + "autocomplete suggestion should target the channel matching search term even if user is member" + ); +}); + +QUnit.test('sidebar channels should be ordered case insensitive alphabetically', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push( + { id: 19, name: "Xyz" }, + { id: 20, name: "abc" }, + { id: 21, name: "Abc" }, + { id: 22, name: "Xyz" } + ); + await this.start(); + const results = document.querySelectorAll('.o_DiscussSidebar_groupChannel .o_DiscussSidebarItem_name'); + assert.deepEqual( + [results[0].textContent, results[1].textContent, results[2].textContent, results[3].textContent], + ["abc", "Abc", "Xyz", "Xyz"], + "Channel name should be in case insensitive alphabetical order" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/discuss/tests/discuss_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_tests.js new file mode 100644 index 00000000..dc2005e5 --- /dev/null +++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js @@ -0,0 +1,4447 @@ +odoo.define('mail/static/src/components/discuss/tests/discuss_tests.js', function (require) { +'use strict'; + +const BusService = require('bus.BusService'); + +const { + afterEach, + afterNextRender, + beforeEach, + nextAnimationFrame, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const Bus = require('web.Bus'); +const { makeTestPromise, file: { createFile, inputFiles } } = require('web.test_utils'); + +const { + applyFilter, + toggleAddCustomFilter, + toggleFilterMenu, +} = require('web.test_utils_control_panel'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('discuss', {}, function () { +QUnit.module('discuss_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { afterEvent, env, widget } = await start(Object.assign({}, params, { + autoOpenDiscuss: true, + data: this.data, + hasDiscuss: true, + })); + this.afterEvent = afterEvent; + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('messaging not initialized', async function (assert) { + assert.expect(1); + + await this.start({ + async mockRPC(route) { + const _super = this._super.bind(this, ...arguments); // limitation of class.js + if (route === '/mail/init_messaging') { + await makeTestPromise(); // simulate messaging never initialized + } + return _super(); + }, + waitUntilMessagingCondition: 'created', + }); + assert.strictEqual( + document.querySelectorAll('.o_Discuss_messagingNotInitialized').length, + 1, + "should display messaging not initialized" + ); +}); + +QUnit.test('messaging becomes initialized', async function (assert) { + assert.expect(2); + + const messagingInitializedProm = makeTestPromise(); + + await this.start({ + async mockRPC(route) { + const _super = this._super.bind(this, ...arguments); // limitation of class.js + if (route === '/mail/init_messaging') { + await messagingInitializedProm; + } + return _super(); + }, + waitUntilMessagingCondition: 'created', + }); + assert.strictEqual( + document.querySelectorAll('.o_Discuss_messagingNotInitialized').length, + 1, + "should display messaging not initialized" + ); + + await afterNextRender(() => messagingInitializedProm.resolve()); + assert.strictEqual( + document.querySelectorAll('.o_Discuss_messagingNotInitialized').length, + 0, + "should no longer display messaging not initialized" + ); +}); + +QUnit.test('basic rendering', async function (assert) { + assert.expect(4); + + await this.start(); + assert.strictEqual( + document.querySelectorAll('.o_Discuss_sidebar').length, + 1, + "should have a sidebar section" + ); + assert.strictEqual( + document.querySelectorAll('.o_Discuss_content').length, + 1, + "should have content section" + ); + assert.strictEqual( + document.querySelectorAll('.o_Discuss_thread').length, + 1, + "should have thread section inside content" + ); + assert.ok( + document.querySelector('.o_Discuss_thread').classList.contains('o_ThreadView'), + "thread section should use ThreadView component" + ); +}); + +QUnit.test('basic rendering: sidebar', async function (assert) { + assert.expect(20); + + await this.start(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_group`).length, + 3, + "should have 3 groups in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupMailbox`).length, + 1, + "should have group 'Mailbox' in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupMailbox .o_DiscussSidebar_groupHeader + `).length, + 0, + "mailbox category should not have any header" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupMailbox .o_DiscussSidebar_item + `).length, + 3, + "should have 3 mailbox items" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupMailbox + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `).length, + 1, + "should have inbox mailbox item" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupMailbox + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + `).length, + 1, + "should have starred mailbox item" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupMailbox + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `).length, + 1, + "should have history mailbox item" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_sidebar .o_DiscussSidebar_separator`).length, + 1, + "should have separator (between mailboxes and channels, but that's not tested)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel`).length, + 1, + "should have group 'Channel' in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupChannel .o_DiscussSidebar_groupHeader + `).length, + 1, + "channel category should have a header" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupChannel .o_DiscussSidebar_groupTitle + `).length, + 1, + "should have title in channel header" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_groupChannel .o_DiscussSidebar_groupTitle + `).textContent.trim(), + "Channels" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_list`).length, + 1, + "channel category should list items" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length, + 0, + "channel category should have no item by default" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChat`).length, + 1, + "should have group 'Chat' in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_groupHeader`).length, + 1, + "channel category should have a header" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_groupTitle`).length, + 1, + "should have title in chat header" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_groupChat .o_DiscussSidebar_groupTitle + `).textContent.trim(), + "Direct Messages" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_list`).length, + 1, + "chat category should list items" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`).length, + 0, + "chat category should have no item by default" + ); +}); + +QUnit.test('sidebar: basic mailbox rendering', async function (assert) { + assert.expect(6); + + await this.start(); + const inbox = document.querySelector(` + .o_DiscussSidebar_groupMailbox + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `); + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator`).length, + 1, + "mailbox should have active indicator" + ); + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_ThreadIcon`).length, + 1, + "mailbox should have an icon" + ); + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_ThreadIcon_mailboxInbox`).length, + 1, + "inbox should have 'inbox' icon" + ); + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_DiscussSidebarItem_name`).length, + 1, + "mailbox should have a name" + ); + assert.strictEqual( + inbox.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent, + "Inbox", + "inbox should have name 'Inbox'" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_counter + `).length, + 0, + "should have no counter when equal to 0 (default value)" + ); +}); + +QUnit.test('sidebar: default active inbox', async function (assert) { + assert.expect(1); + + await this.start(); + const inbox = document.querySelector(` + .o_DiscussSidebar_groupMailbox + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `); + assert.ok( + inbox.querySelector(` + :scope .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "inbox should be active by default" + ); +}); + +QUnit.test('sidebar: change item', async function (assert) { + assert.expect(4); + + await this.start(); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "inbox should be active by default" + ); + assert.notOk( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "starred should be inactive by default" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + `).click() + ); + assert.notOk( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "inbox mailbox should become inactive" + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "starred mailbox should become active"); +}); + +QUnit.test('sidebar: inbox with counter', async function (assert) { + assert.expect(2); + + // notification expected to be counted at init_messaging + this.data['mail.notification'].records.push({ res_partner_id: this.data.currentPartnerId }); + await this.start(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_counter + `).length, + 1, + "should display a counter (= have a counter when different from 0)" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_counter + `).textContent, + "1", + "should have counter value" + ); +}); + +QUnit.test('sidebar: add channel', async function (assert) { + assert.expect(3); + + await this.start(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_groupHeaderItemAdd + `).length, + 1, + "should be able to add channel from header" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_groupHeaderItemAdd + `).title, + "Add or join a channel"); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_groupChannel .o_DiscussSidebar_groupHeaderItemAdd + `).click() + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_itemNew`).length, + 1, + "should have item to add a new channel" + ); +}); + +QUnit.test('sidebar: basic channel rendering', async function (assert) { + assert.expect(14); + + // channel expected to be found in the sidebar, + // with a random unique id and name that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20, name: "General" }); + await this.start(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length, + 1, + "should have one channel item"); + let channel = document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item + `); + assert.strictEqual( + channel.dataset.threadLocalId, + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId, + "should have channel with Id 20" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator`).length, + 1, + "should have active indicator" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator.o-item-active`).length, + 0, + "should not be active by default" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_ThreadIcon`).length, + 1, + "should have an icon" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_name`).length, + 1, + "should have a name" + ); + assert.strictEqual( + channel.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent, + "General", + "should have name value" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commands`).length, + 1, + "should have commands" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_command`).length, + 2, + "should have 2 commands" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commandSettings`).length, + 1, + "should have 'settings' command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commandLeave`).length, + 1, + "should have 'leave' command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_counter`).length, + 0, + "should have a counter when equals 0 (default value)" + ); + + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).click() + ); + channel = document.querySelector(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator.o-item-active`).length, + 1, + "channel should become active" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_ThreadView_composer`).length, + 1, + "should have composer section inside thread content (can post message in channel)" + ); +}); + +QUnit.test('sidebar: channel rendering with needaction counter', async function (assert) { + assert.expect(5); + + // channel expected to be found in the sidebar + // with a random unique id that will be used to link message + this.data['mail.channel'].records.push({ id: 20 }); + // expected needaction message + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], // link message to channel + id: 100, // random unique id, useful to link notification + }); + // expected needaction notification + this.data['mail.notification'].records.push({ + mail_message_id: 100, // id of related message + res_partner_id: this.data.currentPartnerId, // must be for current partner + }); + await this.start(); + const channel = document.querySelector(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_counter`).length, + 1, + "should have a counter when different from 0" + ); + assert.strictEqual( + channel.querySelector(`:scope .o_DiscussSidebarItem_counter`).textContent, + "1", + "should have counter value" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_command`).length, + 1, + "should have single command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commandSettings`).length, + 1, + "should have 'settings' command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarItem_commandLeave`).length, + 0, + "should not have 'leave' command" + ); +}); + +QUnit.test('sidebar: mailing channel', async function (assert) { + assert.expect(1); + + // channel that is expected to be in the sidebar, with proper mass_mailing value + this.data['mail.channel'].records.push({ mass_mailing: true }); + await this.start(); + assert.containsOnce( + document.querySelector(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`), + '.fa.fa-envelope-o', + "should have an icon to indicate that the channel is a mailing channel" + ); +}); + +QUnit.test('sidebar: public/private channel rendering', async function (assert) { + assert.expect(5); + + // channels that are expected to be found in the sidebar (one public, one private) + // with random unique id and name that will be referenced in the test + this.data['mail.channel'].records.push( + { id: 100, name: "channel1", public: 'public', }, + { id: 101, name: "channel2", public: 'private' } + ); + await this.start(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length, + 2, + "should have 2 channel items" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 100, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "should have channel1 (Id 100)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 101, + model: 'mail.channel' + }).localId + }"] + `).length, + 1, + "should have channel2 (Id 101)" + ); + const channel1 = document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 100, + model: 'mail.channel' + }).localId + }"] + `); + const channel2 = document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 101, + model: 'mail.channel' + }).localId + }"] + `); + assert.strictEqual( + channel1.querySelectorAll(`:scope .o_ThreadIcon_channelPublic`).length, + 1, + "channel1 (public) has hashtag icon" + ); + assert.strictEqual( + channel2.querySelectorAll(`:scope .o_ThreadIcon_channelPrivate`).length, + 1, + "channel2 (private) has lock icon" + ); +}); + +QUnit.test('sidebar: basic chat rendering', async function (assert) { + assert.expect(11); + + // expected correspondent, with a random unique id that will be used to link + // partner to chat and a random name that will be asserted in the test + this.data['res.partner'].records.push({ id: 17, name: "Demo" }); + // chat expected to be found in the sidebar + this.data['mail.channel'].records.push({ + channel_type: 'chat', // testing a chat is the goal of the test + id: 10, // random unique id, will be referenced in the test + members: [this.data.currentPartnerId, 17], // expected partners + public: 'private', // expected value for testing a chat + }); + await this.start(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`).length, + 1, + "should have one chat item" + ); + const chat = document.querySelector(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`); + assert.strictEqual( + chat.dataset.threadLocalId, + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel' + }).localId, + "should have chat with Id 10" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_activeIndicator`).length, + 1, + "should have active indicator" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_ThreadIcon`).length, + 1, + "should have an icon" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_name`).length, + 1, + "should have a name" + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent, + "Demo", + "should have correspondent name as name" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commands`).length, + 1, + "should have commands" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_command`).length, + 2, + "should have 2 commands" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commandRename`).length, + 1, + "should have 'rename' command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commandUnpin`).length, + 1, + "should have 'unpin' command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_counter`).length, + 0, + "should have a counter when equals 0 (default value)" + ); +}); + +QUnit.test('sidebar: chat rendering with unread counter', async function (assert) { + assert.expect(5); + + // chat expected to be found in the sidebar + this.data['mail.channel'].records.push({ + channel_type: 'chat', // testing a chat is the goal of the test + id: 10, // random unique id, will be referenced in the test + message_unread_counter: 100, + public: 'private', // expected value for testing a chat + }); + await this.start(); + const chat = document.querySelector(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_counter`).length, + 1, + "should have a counter when different from 0" + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarItem_counter`).textContent, + "100", + "should have counter value" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_command`).length, + 1, + "should have single command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commandRename`).length, + 1, + "should have 'rename' command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_commandUnpin`).length, + 0, + "should not have 'unpin' command" + ); +}); + +QUnit.test('sidebar: chat im_status rendering', async function (assert) { + assert.expect(7); + + // expected correspondent, with a random unique id that will be used to link + // partner to chat, and various im_status values to assert + this.data['res.partner'].records.push( + { id: 101, im_status: 'offline', name: "Partner1" }, + { id: 102, im_status: 'online', name: "Partner2" }, + { id: 103, im_status: 'away', name: "Partner3" } + ); + // chats expected to be found in the sidebar + this.data['mail.channel'].records.push( + { + channel_type: 'chat', // testing a chat is the goal of the test + id: 11, // random unique id, will be referenced in the test + members: [this.data.currentPartnerId, 101], // expected partners + public: 'private', // expected value for testing a chat + }, + { + channel_type: 'chat', // testing a chat is the goal of the test + id: 12, // random unique id, will be referenced in the test + members: [this.data.currentPartnerId, 102], // expected partners + public: 'private', // expected value for testing a chat + }, + { + channel_type: 'chat', // testing a chat is the goal of the test + id: 13, // random unique id, will be referenced in the test + members: [this.data.currentPartnerId, 103], // expected partners + public: 'private', // expected value for testing a chat + } + ); + await this.start(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`).length, + 3, + "should have 3 chat items" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupChat + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 11, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "should have Partner1 (Id 11)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupChat + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 12, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "should have Partner2 (Id 12)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_groupChat + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 13, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "should have Partner3 (Id 13)" + ); + const chat1 = document.querySelector(` + .o_DiscussSidebar_groupChat + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 11, + model: 'mail.channel', + }).localId + }"] + `); + const chat2 = document.querySelector(` + .o_DiscussSidebar_groupChat + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 12, + model: 'mail.channel', + }).localId + }"] + `); + const chat3 = document.querySelector(` + .o_DiscussSidebar_groupChat + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 13, + model: 'mail.channel', + }).localId + }"] + `); + assert.strictEqual( + chat1.querySelectorAll(`:scope .o_ThreadIcon_offline`).length, + 1, + "chat1 should have offline icon" + ); + assert.strictEqual( + chat2.querySelectorAll(`:scope .o_ThreadIcon_online`).length, + 1, + "chat2 should have online icon" + ); + assert.strictEqual( + chat3.querySelectorAll(`:scope .o_ThreadIcon_away`).length, + 1, + "chat3 should have away icon" + ); +}); + +QUnit.test('sidebar: chat custom name', async function (assert) { + assert.expect(1); + + // expected correspondent, with a random unique id that will be used to link + // partner to chat, and a random name not used in the scope of this test but set for consistency + this.data['res.partner'].records.push({ id: 101, name: "Marc Demo" }); + // chat expected to be found in the sidebar + this.data['mail.channel'].records.push({ + channel_type: 'chat', // testing a chat is the goal of the test + custom_channel_name: "Marc", // testing a custom name is the goal of the test + members: [this.data.currentPartnerId, 101], // expected partners + public: 'private', // expected value for testing a chat + }); + await this.start(); + const chat = document.querySelector(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent, + "Marc", + "chat should have custom name as name" + ); +}); + +QUnit.test('sidebar: rename chat', async function (assert) { + assert.expect(8); + + // expected correspondent, with a random unique id that will be used to link + // partner to chat, and a random name not used in the scope of this test but set for consistency + this.data['res.partner'].records.push({ id: 101, name: "Marc Demo" }); + // chat expected to be found in the sidebar + this.data['mail.channel'].records.push({ + channel_type: 'chat', // testing a chat is the goal of the test + custom_channel_name: "Marc", // testing a custom name is the goal of the test + members: [this.data.currentPartnerId, 101], // expected partners + public: 'private', // expected value for testing a chat + }); + await this.start(); + const chat = document.querySelector(`.o_DiscussSidebar_groupChat .o_DiscussSidebar_item`); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent, + "Marc", + "chat should have custom name as name" + ); + assert.notOk( + chat.querySelector(`:scope .o_DiscussSidebarItem_name`).classList.contains('o-editable'), + "chat name should not be editable" + ); + + await afterNextRender(() => + chat.querySelector(`:scope .o_DiscussSidebarItem_commandRename`).click() + ); + assert.ok( + chat.querySelector(`:scope .o_DiscussSidebarItem_name`).classList.contains('o-editable'), + "chat should have editable name" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarItem_nameInput`).length, + 1, + "chat should have editable name input" + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarItem_nameInput`).value, + "Marc", + "editable name input should have custom chat name as value by default" + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarItem_nameInput`).placeholder, + "Marc Demo", + "editable name input should have partner name as placeholder" + ); + + await afterNextRender(() => { + chat.querySelector(`:scope .o_DiscussSidebarItem_nameInput`).value = "Demo"; + const kevt = new window.KeyboardEvent('keydown', { key: "Enter" }); + chat.querySelector(`:scope .o_DiscussSidebarItem_nameInput`).dispatchEvent(kevt); + }); + assert.notOk( + chat.querySelector(`:scope .o_DiscussSidebarItem_name`).classList.contains('o-editable'), + "chat should no longer show editable name" + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarItem_name`).textContent, + "Demo", + "chat should have renamed name as name" + ); +}); + +QUnit.test('default thread rendering', async function (assert) { + assert.expect(16); + + // channel expected to be found in the sidebar, + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `).length, + 1, + "should have inbox mailbox in the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + `).length, + 1, + "should have starred mailbox in the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `).length, + 1, + "should have history mailbox in the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "should have channel 20 in the sidebar" + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `).classList.contains('o-active'), + "inbox mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty + `).length, + 1, + "should have empty thread in inbox" + ); + assert.strictEqual( + document.querySelector(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty + `).textContent.trim(), + "Congratulations, your inbox is empty New messages appear here." + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + `).click() + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + `).classList.contains('o-active'), + "starred mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty + `).length, + 1, + "should have empty thread in starred" + ); + assert.strictEqual( + document.querySelector(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty + `).textContent.trim(), + "No starred messages You can mark any message as 'starred', and it shows up in this mailbox." + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `).click() + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `).classList.contains('o-active'), + "history mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty + `).length, + 1, + "should have empty thread in starred" + ); + assert.strictEqual( + document.querySelector(`.o_Discuss_thread .o_MessageList_empty`).textContent.trim(), + "No history messages Messages marked as read will appear in the history." + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `).click() + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `).classList.contains('o-active'), + "channel 20 should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty + `).length, + 1, + "should have empty thread in starred" + ); + assert.strictEqual( + document.querySelector(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_empty + `).textContent.trim(), + "There are no messages in this conversation." + ); +}); + +QUnit.test('initially load messages from inbox', async function (assert) { + assert.expect(4); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_fetch') { + assert.step('message_fetch'); + assert.strictEqual( + args.kwargs.limit, + 30, + "should fetch up to 30 messages" + ); + assert.deepEqual( + args.kwargs.domain, + [["needaction", "=", true]], + "should fetch needaction messages" + ); + } + return this._super(...arguments); + }, + }); + assert.verifySteps(['message_fetch']); +}); + +QUnit.test('default select thread in discuss params', async function (assert) { + assert.expect(1); + + await this.start({ + discuss: { + params: { + default_active_id: 'mail.box_starred', + }, + } + }); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "starred mailbox should become active" + ); +}); + +QUnit.test('auto-select thread in discuss context', async function (assert) { + assert.expect(1); + + await this.start({ + discuss: { + context: { + active_id: 'mail.box_starred', + }, + }, + }); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "starred mailbox should become active" + ); +}); + +QUnit.test('load single message from channel initially', async function (assert) { + assert.expect(7); + + // channel expected to be rendered, with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + date: "2019-04-20 10:00:00", + id: 100, + model: 'mail.channel', + res_id: 20, + }); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + async mockRPC(route, args) { + if (args.method === 'message_fetch') { + assert.strictEqual( + args.kwargs.limit, + 30, + "should fetch up to 30 messages" + ); + assert.deepEqual( + args.kwargs.domain, + [["channel_ids", "in", [20]]], + "should fetch messages from channel" + ); + } + return this._super(...arguments); + }, + }); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_ThreadView_messageList`).length, + 1, + "should have list of messages" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_MessageList_separatorDate`).length, + 1, + "should have a single date separator" // to check: may be client timezone dependent + ); + assert.strictEqual( + document.querySelector(`.o_Discuss_thread .o_MessageList_separatorLabelDate`).textContent, + "April 20, 2019", + "should display date day of messages" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_MessageList_message`).length, + 1, + "should have a single message" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_MessageList_message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `).length, + 1, + "should have message with Id 100" + ); +}); + +QUnit.test('open channel from active_id as channel id', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start({ + discuss: { + context: { + active_id: 20, + }, + } + }); + assert.containsOnce( + document.body, + ` + .o_Discuss_thread[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ id: 20, model: 'mail.channel' }).localId + }"] + `, + "should have channel with ID 20 open in Discuss when providing active_id 20" + ); +}); + +QUnit.test('basic rendering of message', async function (assert) { + // AKU TODO: should be in message-only tests + assert.expect(13); + + // channel expected to be rendered, with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + // partner to be set as author, with a random unique id that will be used to + // link message and a random name that will be asserted in the test + this.data['res.partner'].records.push({ id: 11, name: "Demo" }); + this.data['mail.message'].records.push({ + author_id: 11, + body: "<p>body</p>", + channel_ids: [20], + date: "2019-04-20 10:00:00", + id: 100, + model: 'mail.channel', + res_id: 20, + }); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + const message = document.querySelector(` + .o_Discuss_thread + .o_ThreadView_messageList + .o_MessageList_message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_sidebar`).length, + 1, + "should have message sidebar of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_authorAvatar`).length, + 1, + "should have author avatar in sidebar of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_Message_authorAvatar`).dataset.src, + "/web/image/res.partner/11/image_128", + "should have url of message in author avatar sidebar" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_core`).length, + 1, + "should have core part of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_header`).length, + 1, + "should have header in core part of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_authorName`).length, + 1, + "should have author name in header of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_Message_authorName`).textContent, + "Demo", + "should have textually author name in header of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_header .o_Message_date`).length, + 1, + "should have date in header of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_header .o_Message_commands`).length, + 1, + "should have commands in header of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_header .o_Message_command`).length, + 1, + "should have a single command in header of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_commandStar`).length, + 1, + "should have command to star message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_content`).length, + 1, + "should have content in core part of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_Message_content`).textContent.trim(), + "body", + "should have body of message in content part of message" + ); +}); + +QUnit.test('basic rendering of squashed message', async function (assert) { + // messages are squashed when "close", e.g. less than 1 minute has elapsed + // from messages of same author and same thread. Note that this should + // be working in non-mailboxes + // AKU TODO: should be message and/or message list-only tests + assert.expect(12); + + // channel expected to be rendered, with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + // partner to be set as author, with a random unique id that will be used to link message + this.data['res.partner'].records.push({ id: 11 }); + this.data['mail.message'].records.push( + { + author_id: 11, // must be same author as other message + body: "<p>body1</p>", // random body, set for consistency + channel_ids: [20], // to link message to channel + date: "2019-04-20 10:00:00", // date must be within 1 min from other message + id: 100, // random unique id, will be referenced in the test + message_type: 'comment', // must be a squash-able type- + model: 'mail.channel', // to link message to channel + res_id: 20, // id of related channel + }, + { + author_id: 11, // must be same author as other message + body: "<p>body2</p>", // random body, will be asserted in the test + channel_ids: [20], // to link message to channel + date: "2019-04-20 10:00:30", // date must be within 1 min from other message + id: 101, // random unique id, will be referenced in the test + message_type: 'comment', // must be a squash-able type + model: 'mail.channel', // to link message to channel + res_id: 20, // id of related channel + } + ); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 2, + "should have 2 messages" + ); + const message1 = document.querySelector(` + .o_Discuss_thread + .o_ThreadView_messageList + .o_MessageList_message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `); + const message2 = document.querySelector(` + .o_Discuss_thread + .o_ThreadView_messageList + .o_MessageList_message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + `); + assert.notOk( + message1.classList.contains('o-squashed'), + "message 1 should not be squashed" + ); + assert.notOk( + message1.querySelector(`:scope .o_Message_sidebar`).classList.contains('o-message-squashed'), + "message 1 should not have squashed sidebar" + ); + assert.ok( + message2.classList.contains('o-squashed'), + "message 2 should be squashed" + ); + assert.ok( + message2.querySelector(`:scope .o_Message_sidebar`).classList.contains('o-message-squashed'), + "message 2 should have squashed sidebar" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_Message_sidebar .o_Message_date`).length, + 1, + "message 2 should have date in sidebar" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_Message_sidebar .o_Message_commands`).length, + 1, + "message 2 should have some commands in sidebar" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_Message_sidebar .o_Message_commandStar`).length, + 1, + "message 2 should have star command in sidebar" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_Message_core`).length, + 1, + "message 2 should have core part" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_Message_header`).length, + 0, + "message 2 should have a header in core part" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_Message_content`).length, + 1, + "message 2 should have some content in core part" + ); + assert.strictEqual( + message2.querySelector(`:scope .o_Message_content`).textContent.trim(), + "body2", + "message 2 should have body in content part" + ); +}); + +QUnit.test('inbox messages are never squashed', async function (assert) { + assert.expect(3); + + // partner to be set as author, with a random unique id that will be used to link message + this.data['res.partner'].records.push({ id: 11 }); + this.data['mail.message'].records.push( + { + author_id: 11, // must be same author as other message + body: "<p>body1</p>", // random body, set for consistency + channel_ids: [20], // to link message to channel + date: "2019-04-20 10:00:00", // date must be within 1 min from other message + id: 100, // random unique id, will be referenced in the test + message_type: 'comment', // must be a squash-able type- + model: 'mail.channel', // to link message to channel + needaction: true, // necessary for message_fetch domain + needaction_partner_ids: [this.data.currentPartnerId], // for consistency + res_id: 20, // id of related channel + }, + { + author_id: 11, // must be same author as other message + body: "<p>body2</p>", // random body, will be asserted in the test + channel_ids: [20], // to link message to channel + date: "2019-04-20 10:00:30", // date must be within 1 min from other message + id: 101, // random unique id, will be referenced in the test + message_type: 'comment', // must be a squash-able type + model: 'mail.channel', // to link message to channel + needaction: true, // necessary for message_fetch domain + needaction_partner_ids: [this.data.currentPartnerId], // for consistency + res_id: 20, // id of related channel + } + ); + await this.start(); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 2, + "should have 2 messages" + ); + const message1 = document.querySelector(` + .o_Discuss_thread + .o_ThreadView_messageList + .o_MessageList_message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `); + const message2 = document.querySelector(` + .o_Discuss_thread + .o_ThreadView_messageList + .o_MessageList_message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + `); + assert.notOk( + message1.classList.contains('o-squashed'), + "message 1 should not be squashed" + ); + assert.notOk( + message2.classList.contains('o-squashed'), + "message 2 should not be squashed" + ); +}); + +QUnit.test('load all messages from channel initially, less than fetch limit (29 < 30)', async function (assert) { + // AKU TODO: thread specific test + assert.expect(5); + + // channel expected to be rendered, with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + // partner to be set as author, with a random unique id that will be used to link message + this.data['res.partner'].records.push({ id: 11 }); + for (let i = 28; i >= 0; i--) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + date: "2019-04-20 10:00:00", + model: 'mail.channel', + res_id: 20, + }); + } + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + async mockRPC(route, args) { + if (args.method === 'message_fetch') { + assert.strictEqual(args.kwargs.limit, 30, "should fetch up to 30 messages"); + } + return this._super(...arguments); + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_separatorDate + `).length, + 1, + "should have a single date separator" // to check: may be client timezone dependent + ); + assert.strictEqual( + document.querySelector(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_separatorLabelDate + `).textContent, + "April 20, 2019", + "should display date day of messages" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 29, + "should have 29 messages" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_loadMore + `).length, + 0, + "should not have load more link" + ); +}); + +QUnit.test('load more messages from channel', async function (assert) { + // AKU: thread specific test + assert.expect(6); + + // channel expected to be rendered, with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + // partner to be set as author, with a random unique id that will be used to link message + this.data['res.partner'].records.push({ id: 11 }); + for (let i = 0; i < 40; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + date: "2019-04-20 10:00:00", + model: 'mail.channel', + res_id: 20, + }); + } + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_separatorDate + `).length, + 1, + "should have a single date separator" // to check: may be client timezone dependent + ); + assert.strictEqual( + document.querySelector(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_separatorLabelDate + `).textContent, + "April 20, 2019", + "should display date day of messages" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 30, + "should have 30 messages" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_loadMore + `).length, + 1, + "should have load more link" + ); + + await afterNextRender(() => + document.querySelector(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_loadMore + `).click() + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 40, + "should have 40 messages" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_loadMore + `).length, + 0, + "should not longer have load more link (all messages loaded)" + ); +}); + +QUnit.test('auto-scroll to bottom of thread', async function (assert) { + // AKU TODO: thread specific test + assert.expect(2); + + // channel expected to be rendered, with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + for (let i = 1; i <= 25; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + model: 'mail.channel', + res_id: 20, + }); + } + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + waitUntilEvent: { + eventName: 'o-component-message-list-scrolled', + message: "should wait until channel 20 scrolled to its last message initially", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector('.o_ThreadView_messageList'); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 25, + "should have 25 messages" + ); + const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`); + assert.strictEqual( + messageList.scrollTop, + messageList.scrollHeight - messageList.clientHeight, + "should have scrolled to bottom of thread" + ); +}); + +QUnit.test('load more messages from channel (auto-load on scroll)', async function (assert) { + // AKU TODO: thread specific test + assert.expect(3); + + this.data['mail.channel'].records.push({ id: 20 }); + for (let i = 0; i < 40; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + model: 'mail.channel', + res_id: 20, + }); + } + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + waitUntilEvent: { + eventName: 'o-component-message-list-scrolled', + message: "should wait until channel 20 scrolled to its last message initially", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 30, + "should have 30 messages" + ); + + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => document.querySelector('.o_ThreadView_messageList').scrollTop = 0, + message: "should wait until channel 20 loaded more messages after scrolling to top", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'more-messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 40, + "should have 40 messages" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Dsiscuss_thread .o_ThreadView_messageList .o_MessageList_loadMore + `).length, + 0, + "should not longer have load more link (all messages loaded)" + ); +}); + +QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) { + // this test requires several messages so that the last message is not + // visible. This is necessary in order to display 'new messages' and not + // remove from DOM right away from seeing last message. + // AKU TODO: thread specific test + assert.expect(6); + + // Needed partner & user to allow simulation of message reception + this.data['res.partner'].records.push({ + id: 11, + name: "Foreigner partner", + }); + this.data['res.users'].records.push({ + id: 42, + name: "Foreigner user", + partner_id: 11, + }); + // channel expected to be rendered, with a random unique id that will be + // referenced in the test and the seen_message_id value set to last message + this.data['mail.channel'].records.push({ + id: 20, + seen_message_id: 125, + uuid: 'randomuuid', + }); + for (let i = 1; i <= 25; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + id: 100 + i, // for setting proper value for seen_message_id + model: 'mail.channel', + res_id: 20, + }); + } + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + waitUntilEvent: { + eventName: 'o-component-message-list-scrolled', + message: "should wait until channel 20 scrolled to its last message initially", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }, + }); + assert.containsN( + document.body, + '.o_MessageList_message', + 25, + "should have 25 messages" + ); + assert.containsNone( + document.body, + '.o_MessageList_separatorNewMessages', + "should not display 'new messages' separator" + ); + // scroll to top + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0; + }, + message: "should wait until channel scrolled to top", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === 0 + ); + }, + }); + // composer is focused by default, we remove that focus + document.querySelector('.o_ComposerTextInput_textarea').blur(); + // simulate receiving a message + await afterNextRender(async () => this.env.services.rpc({ + route: '/mail/chat_post', + params: { + context: { + mockedUserId: 42, + }, + message_content: "hu", + uuid: 'randomuuid', + }, + })); + + assert.containsN( + document.body, + '.o_MessageList_message', + 26, + "should have 26 messages" + ); + assert.containsOnce( + document.body, + '.o_MessageList_separatorNewMessages', + "should display 'new messages' separator" + ); + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`); + messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight; + }, + message: "should wait until channel scrolled to bottom", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }); + assert.containsOnce( + document.body, + '.o_MessageList_separatorNewMessages', + "should still display 'new messages' separator as composer is not focused" + ); + + await afterNextRender(() => + document.querySelector('.o_ComposerTextInput_textarea').focus() + ); + assert.containsNone( + document.body, + '.o_MessageList_separatorNewMessages', + "should no longer display 'new messages' separator (message seen)" + ); +}); + +QUnit.test('restore thread scroll position', async function (assert) { + assert.expect(6); + // channels expected to be rendered, with random unique id that will be referenced in the test + this.data['mail.channel'].records.push( + { + id: 11, + }, + { + id: 12, + }, + ); + for (let i = 1; i <= 25; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [11], + model: 'mail.channel', + res_id: 11, + }); + } + for (let i = 1; i <= 24; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [12], + model: 'mail.channel', + res_id: 12, + }); + } + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_11', + }, + }, + waitUntilEvent: { + eventName: 'o-component-message-list-scrolled', + message: "should wait until channel 11 scrolled to its last message", + predicate: ({ thread }) => { + return thread && thread.model === 'mail.channel' && thread.id === 11; + }, + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 25, + "should have 25 messages in channel 11" + ); + const initialMessageList = document.querySelector(` + .o_Discuss_thread + .o_ThreadView_messageList + `); + assert.strictEqual( + initialMessageList.scrollTop, + initialMessageList.scrollHeight - initialMessageList.clientHeight, + "should have scrolled to bottom of channel 11 initially" + ); + + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0, + message: "should wait until channel 11 changed its scroll position to top", + predicate: ({ thread }) => { + return thread && thread.model === 'mail.channel' && thread.id === 11; + }, + }); + assert.strictEqual( + document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop, + 0, + "should have scrolled to top of channel 11", + ); + + // Ensure scrollIntoView of channel 12 has enough time to complete before + // going back to channel 11. Await is needed to prevent the scrollIntoView + // initially planned for channel 12 to actually apply on channel 11. + // task-2333535 + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + // select channel 12 + document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 12, + model: 'mail.channel', + }).localId + }"] + `).click(); + }, + message: "should wait until channel 12 scrolled to its last message", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector('.o_ThreadView_messageList'); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 12 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message + `).length, + 24, + "should have 24 messages in channel 12" + ); + + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + // select channel 11 + document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 11, + model: 'mail.channel', + }).localId + }"] + `).click(); + }, + message: "should wait until channel 11 restored its scroll position", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 11 && + scrollTop === 0 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop, + 0, + "should have recovered scroll position of channel 11 (scroll to top)" + ); + + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + // select channel 12 + document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 12, + model: 'mail.channel', + }).localId + }"] + `).click(); + }, + message: "should wait until channel 12 recovered its scroll position (to bottom)", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector('.o_ThreadView_messageList'); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 12 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }); + const messageList = document.querySelector('.o_ThreadView_messageList'); + assert.strictEqual( + messageList.scrollTop, + messageList.scrollHeight - messageList.clientHeight, + "should have recovered scroll position of channel 12 (scroll to bottom)" + ); +}); + +QUnit.test('message origin redirect to channel', async function (assert) { + assert.expect(15); + + // channels expected to be rendered, with random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 11 }, { id: 12 }); + this.data['mail.message'].records.push( + { + body: "not empty", + channel_ids: [11, 12], + id: 100, + model: 'mail.channel', + record_name: "channel11", + res_id: 11, + }, + { + body: "not empty", + channel_ids: [11, 12], + id: 101, + model: 'mail.channel', + record_name: "channel12", + res_id: 12, + } + ); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_11', + }, + }, + }); + assert.strictEqual( + document.querySelectorAll('.o_Discuss_thread .o_Message').length, + 2, + "should have 2 messages" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `).length, + 1, + "should have message1 (Id 100)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + `).length, + 1, + "should have message2 (Id 101)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + .o_Message_originThread + `).length, + 0, + "message1 should not have origin part in channel11 (same origin as channel)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + .o_Message_originThread + `).length, + 1, + "message2 should have origin part (origin is channel12 !== channel11)" + ); + assert.strictEqual( + document.querySelector(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + .o_Message_originThread + `).textContent.trim(), + "(from #channel12)", + "message2 should display name of origin channel" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + .o_Message_originThreadLink + `).length, + 1, + "message2 should have link to redirect to origin" + ); + + // click on origin link of message2 (= channel12) + await afterNextRender(() => + document.querySelector(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + .o_Message_originThreadLink + `).click() + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 12, + model: 'mail.channel', + }).localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "channel12 should be active channel on redirect from discuss app" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_Message`).length, + 2, + "should have 2 messages" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `).length, + 1, + "should have message1 (Id 100)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + `).length, + 1, + "should have message2 (Id 101)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + .o_Message_originThread + `).length, + 1, + "message1 should have origin thread part (= channel11 !== channel12)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + .o_Message_originThread + `).length, + 0, + "message2 should not have origin thread part in channel12 (same as current channel)" + ); + assert.strictEqual( + document.querySelector(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + .o_Message_originThread + `).textContent.trim(), + "(from #channel11)", + "message1 should display name of origin channel" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + .o_Message_originThreadLink + `).length, + 1, + "message1 should have link to redirect to origin channel" + ); +}); + +QUnit.test('redirect to author (open chat)', async function (assert) { + assert.expect(7); + + // expected correspondent, with a random unique id that will be used to link + // partner to chat and a random name that will be asserted in the test + this.data['res.partner'].records.push({ id: 7, name: "Demo" }); + this.data['res.users'].records.push({ partner_id: 7 }); + this.data['mail.channel'].records.push( + // channel expected to be found in the sidebar + { + id: 1, // random unique id, will be referenced in the test + name: "General", // random name, will be asserted in the test + }, + // chat expected to be found in the sidebar + { + channel_type: 'chat', // testing a chat is the goal of the test + id: 10, // random unique id, will be referenced in the test + members: [this.data.currentPartnerId, 7], // expected partners + public: 'private', // expected value for testing a chat + } + ); + this.data['mail.message'].records.push( + { + author_id: 7, + body: "not empty", + channel_ids: [1], + id: 100, + model: 'mail.channel', + res_id: 1, + } + ); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_1', + }, + }, + }); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 1, + model: 'mail.channel', + }).localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "channel 'General' should be active" + ); + assert.notOk( + document.querySelector(` + .o_DiscussSidebar_groupChat + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "Chat 'Demo' should not be active" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_Message`).length, + 1, + "should have 1 message" + ); + const msg1 = document.querySelector(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `); + assert.strictEqual( + msg1.querySelectorAll(`:scope .o_Message_authorAvatar`).length, + 1, + "message1 should have author image" + ); + assert.ok( + msg1.querySelector(`:scope .o_Message_authorAvatar`).classList.contains('o_redirect'), + "message1 should have redirect to author" + ); + + await afterNextRender(() => + msg1.querySelector(`:scope .o_Message_authorAvatar`).click() + ); + assert.notOk( + document.querySelector(` + .o_DiscussSidebar_groupChannel + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 1, + model: 'mail.channel', + }).localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "channel 'General' should become inactive after author redirection" + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_groupChat + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + .o_DiscussSidebarItem_activeIndicator + `).classList.contains('o-item-active'), + "chat 'Demo' should become active after author redirection" + ); +}); + +QUnit.test('sidebar quick search', async function (assert) { + // feature enables at 20 or more channels + assert.expect(6); + + for (let id = 1; id <= 20; id++) { + this.data['mail.channel'].records.push({ id, name: `channel${id}` }); + } + await this.start(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length, + 20, + "should have 20 channel items" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_sidebar input.o_DiscussSidebar_quickSearch`).length, + 1, + "should have quick search in sidebar" + ); + + const quickSearch = document.querySelector(` + .o_Discuss_sidebar input.o_DiscussSidebar_quickSearch + `); + await afterNextRender(() => { + quickSearch.value = "1"; + const kevt1 = new window.KeyboardEvent('input'); + quickSearch.dispatchEvent(kevt1); + }); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length, + 11, + "should have filtered to 11 channel items" + ); + + await afterNextRender(() => { + quickSearch.value = "12"; + const kevt2 = new window.KeyboardEvent('input'); + quickSearch.dispatchEvent(kevt2); + }); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length, + 1, + "should have filtered to a single channel item" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_groupChannel .o_DiscussSidebar_item + `).dataset.threadLocalId, + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 12, + model: 'mail.channel', + }).localId, + "should have filtered to a single channel item with Id 12" + ); + + await afterNextRender(() => { + quickSearch.value = "123"; + const kevt3 = new window.KeyboardEvent('input'); + quickSearch.dispatchEvent(kevt3); + }); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebar_groupChannel .o_DiscussSidebar_item`).length, + 0, + "should have filtered to no channel item" + ); +}); + +QUnit.test('basic control panel rendering', async function (assert) { + assert.expect(8); + + // channel expected to be found in the sidebar + // with a random unique id and name that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20, name: "General" }); + await this.start(); + assert.strictEqual( + document.querySelector(` + .o_widget_Discuss .o_control_panel .breadcrumb + `).textContent, + "Inbox", + "display inbox in the breadcrumb" + ); + const markAllReadButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonMarkAllRead`); + assert.isVisible( + markAllReadButton, + "should have visible button 'Mark all read' in the control panel of inbox" + ); + assert.ok( + markAllReadButton.disabled, + "should have disabled button 'Mark all read' in the control panel of inbox (no messages)" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + `).click() + ); + assert.strictEqual( + document.querySelector(` + .o_widget_Discuss .o_control_panel .breadcrumb + `).textContent, + "Starred", + "display starred in the breadcrumb" + ); + const unstarAllButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonUnstarAll`); + assert.isVisible( + unstarAllButton, + "should have visible button 'Unstar all' in the control panel of starred" + ); + assert.ok( + unstarAllButton.disabled, + "should have disabled button 'Unstar all' in the control panel of starred (no messages)" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `).click() + ); + assert.strictEqual( + document.querySelector(` + .o_widget_Discuss .o_control_panel .breadcrumb + `).textContent, + "#General", + "display general in the breadcrumb" + ); + const inviteButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonInvite`); + assert.isVisible( + inviteButton, + "should have visible button 'Invite' in the control panel of channel" + ); +}); + +QUnit.test('inbox: mark all messages as read', async function (assert) { + assert.expect(8); + + // channel expected to be found in the sidebar, + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + this.data['mail.message'].records.push( + // first expected message + { + body: "not empty", + channel_ids: [20], // link message to channel + id: 100, // random unique id, useful to link notification + model: 'mail.channel', + // needaction needs to be set here for message_fetch domain, because + // mocked models don't have computed fields + needaction: true, + res_id: 20, + }, + // second expected message + { + body: "not empty", + channel_ids: [20], // link message to channel + id: 101, // random unique id, useful to link notification + model: 'mail.channel', + // needaction needs to be set here for message_fetch domain, because + // mocked models don't have computed fields + needaction: true, + res_id: 20, + } + ); + this.data['mail.notification'].records.push( + // notification to have first message in inbox + { + mail_message_id: 100, // id of related message + res_partner_id: this.data.currentPartnerId, // must be for current partner + }, + // notification to have second message in inbox + { + mail_message_id: 101, // id of related message + res_partner_id: this.data.currentPartnerId, // must be for current partner + } + ); + await this.start(); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_counter + `).textContent, + "2", + "inbox should have counter of 2" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + .o_DiscussSidebarItem_counter + `).textContent, + "2", + "channel should have counter of 2" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss .o_Message`).length, + 2, + "should have 2 messages in inbox" + ); + let markAllReadButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonMarkAllRead`); + assert.notOk( + markAllReadButton.disabled, + "should have enabled button 'Mark all read' in the control panel of inbox (has messages)" + ); + + await afterNextRender(() => markAllReadButton.click()); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_counter + `).length, + 0, + "inbox should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + .o_DiscussSidebarItem_counter + `).length, + 0, + "channel should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss .o_Message`).length, + 0, + "should have no message in inbox" + ); + markAllReadButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonMarkAllRead`); + assert.ok( + markAllReadButton.disabled, + "should have disabled button 'Mark all read' in the control panel of inbox (no messages)" + ); +}); + +QUnit.test('starred: unstar all', async function (assert) { + assert.expect(6); + + // messages expected to be starred + this.data['mail.message'].records.push( + { body: "not empty", starred_partner_ids: [this.data.currentPartnerId] }, + { body: "not empty", starred_partner_ids: [this.data.currentPartnerId] } + ); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.box_starred', + }, + }, + }); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + .o_DiscussSidebarItem_counter + `).textContent, + "2", + "starred should have counter of 2" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss .o_Message`).length, + 2, + "should have 2 messages in starred" + ); + let unstarAllButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonUnstarAll`); + assert.notOk( + unstarAllButton.disabled, + "should have enabled button 'Unstar all' in the control panel of starred (has messages)" + ); + + await afterNextRender(() => unstarAllButton.click()); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + .o_DiscussSidebarItem_counter + `).length, + 0, + "starred should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss .o_Message`).length, + 0, + "should have no message in starred" + ); + unstarAllButton = document.querySelector(`.o_widget_Discuss_controlPanelButtonUnstarAll`); + assert.ok( + unstarAllButton.disabled, + "should have disabled button 'Unstar all' in the control panel of starred (no messages)" + ); +}); + +QUnit.test('toggle_star message', async function (assert) { + assert.expect(16); + + // channel expected to be initially rendered + // with a random unique id, will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + id: 100, + model: 'mail.channel', + res_id: 20, + }); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + async mockRPC(route, args) { + if (args.method === 'toggle_message_starred') { + assert.step('rpc:toggle_message_starred'); + assert.strictEqual( + args.args[0][0], + 100, + "should have message Id in args" + ); + } + return this._super(...arguments); + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + .o_DiscussSidebarItem_counter + `).length, + 0, + "starred should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss .o_Message`).length, + 1, + "should have 1 message in channel" + ); + let message = document.querySelector(`.o_Discuss .o_Message`); + assert.notOk( + message.classList.contains('o-starred'), + "message should not be starred" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_commandStar`).length, + 1, + "message should have star command" + ); + + await afterNextRender(() => message.querySelector(`:scope .o_Message_commandStar`).click()); + assert.verifySteps(['rpc:toggle_message_starred']); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + .o_DiscussSidebarItem_counter + `).textContent, + "1", + "starred should display a counter of 1" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss .o_Message`).length, + 1, + "should have kept 1 message in channel" + ); + message = document.querySelector(`.o_Discuss .o_Message`); + assert.ok( + message.classList.contains('o-starred'), + "message should be starred" + ); + + await afterNextRender(() => message.querySelector(`:scope .o_Message_commandStar`).click()); + assert.verifySteps(['rpc:toggle_message_starred']); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.starred.localId + }"] + .o_DiscussSidebarItem_counter + `).length, + 0, + "starred should no longer display a counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss .o_Message`).length, + 1, + "should still have 1 message in channel" + ); + message = document.querySelector(`.o_Discuss .o_Message`); + assert.notOk( + message.classList.contains('o-starred'), + "message should no longer be starred" + ); +}); + +QUnit.test('composer state: text save and restore', async function (assert) { + assert.expect(2); + + // channels expected to be found in the sidebar, + // with random unique id and name that will be referenced in the test + this.data['mail.channel'].records.push( + { id: 20, name: "General" }, + { id: 21, name: "Special" } + ); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + // Write text in composer for #general + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "A message"); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('input')); + }); + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebarItem[data-thread-name="Special"]`).click() + ); + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "An other message"); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('input')); + }); + // Switch back to #general + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebarItem[data-thread-name="General"]`).click() + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "A message", + "should restore the input text" + ); + + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebarItem[data-thread-name="Special"]`).click() + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "An other message", + "should restore the input text" + ); +}); + +QUnit.test('composer state: attachments save and restore', async function (assert) { + assert.expect(6); + + // channels expected to be found in the sidebar + // with random unique id and name that will be referenced in the test + this.data['mail.channel'].records.push( + { id: 20, name: "General" }, + { id: 21, name: "Special" } + ); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + const channels = document.querySelectorAll(` + .o_DiscussSidebar_groupChannel .o_DiscussSidebar_item + `); + // Add attachment in a message for #general + await afterNextRender(async () => { + const file = await createFile({ + content: 'hello, world', + contentType: 'text/plain', + name: 'text.txt', + }); + inputFiles( + document.querySelector('.o_FileUploader_input'), + [file] + ); + }); + // Switch to #special + await afterNextRender(() => channels[1].click()); + // Add attachments in a message for #special + const files = [ + await createFile({ + content: 'hello2, world', + contentType: 'text/plain', + name: 'text2.txt', + }), + await createFile({ + content: 'hello3, world', + contentType: 'text/plain', + name: 'text3.txt', + }), + await createFile({ + content: 'hello4, world', + contentType: 'text/plain', + name: 'text4.txt', + }), + ]; + await afterNextRender(() => + inputFiles( + document.querySelector('.o_FileUploader_input'), + files + ) + ); + // Switch back to #general + await afterNextRender(() => channels[0].click()); + // Check attachment is reloaded + assert.strictEqual( + document.querySelectorAll(`.o_Composer .o_Attachment`).length, + 1, + "should have 1 attachment in the composer" + ); + assert.strictEqual( + document.querySelector(`.o_Composer .o_Attachment`).dataset.attachmentLocalId, + this.env.models['mail.attachment'].findFromIdentifyingData({ id: 1 }).localId, + "should have correct 1st attachment in the composer" + ); + + // Switch back to #special + await afterNextRender(() => channels[1].click()); + // Check attachments are reloaded + assert.strictEqual( + document.querySelectorAll(`.o_Composer .o_Attachment`).length, + 3, + "should have 3 attachments in the composer" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Composer .o_Attachment`)[0].dataset.attachmentLocalId, + this.env.models['mail.attachment'].findFromIdentifyingData({ id: 2 }).localId, + "should have attachment with id 2 as 1st attachment" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Composer .o_Attachment`)[1].dataset.attachmentLocalId, + this.env.models['mail.attachment'].findFromIdentifyingData({ id: 3 }).localId, + "should have attachment with id 3 as 2nd attachment" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Composer .o_Attachment`)[2].dataset.attachmentLocalId, + this.env.models['mail.attachment'].findFromIdentifyingData({ id: 4 }).localId, + "should have attachment with id 4 as 3rd attachment" + ); +}); + +QUnit.test('post a simple message', async function (assert) { + assert.expect(15); + + // channel expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + let postedMessageId; + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + async mockRPC(route, args) { + const res = await this._super(...arguments); + if (args.method === 'message_post') { + assert.step('message_post'); + assert.strictEqual( + args.args[0], + 20, + "should post message to channel Id 20" + ); + assert.strictEqual( + args.kwargs.body, + "Test", + "should post with provided content in composer input" + ); + assert.strictEqual( + args.kwargs.message_type, + "comment", + "should set message type as 'comment'" + ); + assert.strictEqual( + args.kwargs.subtype_xmlid, + "mail.mt_comment", + "should set subtype_xmlid as 'comment'" + ); + postedMessageId = res; + } + return res; + }, + }); + assert.strictEqual( + document.querySelectorAll(`.o_MessageList_empty`).length, + 1, + "should display thread with no message initially" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Message`).length, + 0, + "should display no message initially" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "", + "should have empty content initially" + ); + + // insert some HTML in editable + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "Test"); + }); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "Test", + "should have inserted text in editable" + ); + + await afterNextRender(() => + document.querySelector('.o_Composer_buttonSend').click() + ); + assert.verifySteps(['message_post']); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "", + "should have no content in composer input after posting message" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Message`).length, + 1, + "should display a message after posting message" + ); + const message = document.querySelector(`.o_Message`); + assert.strictEqual( + message.dataset.messageLocalId, + this.env.models['mail.message'].findFromIdentifyingData({ id: postedMessageId }).localId, + "new message in thread should be linked to newly created message from message post" + ); + assert.strictEqual( + message.querySelector(`:scope .o_Message_authorName`).textContent, + "Mitchell Admin", + "new message in thread should be from current partner name" + ); + assert.strictEqual( + message.querySelector(`:scope .o_Message_content`).textContent, + "Test", + "new message in thread should have content typed from composer text input" + ); +}); + +QUnit.test('post message on non-mailing channel with "Enter" keyboard shortcut', async function (assert) { + assert.expect(2); + + // channel expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20, mass_mailing: false }); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should not have any message initially in channel" + ); + + // insert some HTML in editable + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "Test"); + }); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent('keydown', { key: "Enter" }); + document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + '.o_Message', + "should now have single message in channel after posting message from pressing 'Enter' in text input of composer" + ); +}); + +QUnit.test('do not post message on non-mailing channel with "SHIFT-Enter" keyboard shortcut', async function (assert) { + // Note that test doesn't assert SHIFT-Enter makes a newline, because this + // default browser cannot be simulated with just dispatching + // programmatically crafted events... + assert.expect(2); + + // channel expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20, mass_mailing: true }); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should not have any message initially in channel" + ); + + // insert some HTML in editable + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "Test"); + }); + const kevt = new window.KeyboardEvent('keydown', { key: "Enter", shiftKey: true }); + document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(kevt); + await nextAnimationFrame(); + assert.containsNone( + document.body, + '.o_Message', + "should still not have any message in channel after pressing 'Shift-Enter' in text input of composer" + ); +}); + +QUnit.test('post message on mailing channel with "CTRL-Enter" keyboard shortcut', async function (assert) { + assert.expect(2); + + // channel expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20, mass_mailing: true }); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should not have any message initially in channel" + ); + + // insert some HTML in editable + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "Test"); + }); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent('keydown', { ctrlKey: true, key: "Enter" }); + document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + '.o_Message', + "should now have single message in channel after posting message from pressing 'CTRL-Enter' in text input of composer" + ); +}); + +QUnit.test('post message on mailing channel with "META-Enter" keyboard shortcut', async function (assert) { + assert.expect(2); + + // channel expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20, mass_mailing: true }); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should not have any message initially in channel" + ); + + // insert some HTML in editable + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "Test"); + }); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent('keydown', { key: "Enter", metaKey: true }); + document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + '.o_Message', + "should now have single message in channel after posting message from pressing 'META-Enter' in text input of composer" + ); +}); + +QUnit.test('do not post message on mailing channel with "Enter" keyboard shortcut', async function (assert) { + // Note that test doesn't assert Enter makes a newline, because this + // default browser cannot be simulated with just dispatching + // programmatically crafted events... + assert.expect(2); + + // channel expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20, mass_mailing: true }); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.channel_20', + }, + }, + }); + assert.containsNone( + document.body, + '.o_Message', + "should not have any message initially in mailing channel" + ); + + // insert some HTML in editable + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, "Test"); + }); + const kevt = new window.KeyboardEvent('keydown', { key: "Enter" }); + document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(kevt); + await nextAnimationFrame(); + assert.containsNone( + document.body, + '.o_Message', + "should still not have any message in mailing channel after pressing 'Enter' in text input of composer" + ); +}); + +QUnit.test('rendering of inbox message', async function (assert) { + // AKU TODO: kinda message specific test + assert.expect(7); + + this.data['mail.message'].records.push({ + body: "not empty", + model: 'res.partner', // random existing model + needaction: true, // for message_fetch domain + needaction_partner_ids: [this.data.currentPartnerId], // for consistency + record_name: 'Refactoring', // random name, will be asserted in the test + res_id: 20, // random related id + }); + await this.start(); + assert.strictEqual( + document.querySelectorAll('.o_Message').length, + 1, + "should display a message" + ); + const message = document.querySelector('.o_Message'); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_originThread`).length, + 1, + "should display origin thread of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_Message_originThread`).textContent, + " on Refactoring", + "should display origin thread name" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_command`).length, + 3, + "should display 3 commands" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_commandStar`).length, + 1, + "should display star command" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_commandReply`).length, + 1, + "should display reply command" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_Message_commandMarkAsRead`).length, + 1, + "should display mark as read command" + ); +}); + +QUnit.test('mark channel as seen on last message visible [REQUIRE FOCUS]', async function (assert) { + assert.expect(3); + + // channel expected to be found in the sidebar, with the expected message_unread_counter + // and a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 10, message_unread_counter: 1 }); + this.data['mail.message'].records.push({ + id: 12, + body: "not empty", + channel_ids: [10], + model: 'mail.channel', + res_id: 10, + }); + await this.start(); + assert.containsOnce( + document.body, + `.o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"]`, + "should have discuss sidebar item with the channel" + ); + assert.hasClass( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `), + 'o-unread', + "sidebar item of channel ID 10 should be unread" + ); + + await afterNextRender(() => this.afterEvent({ + eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', + func: () => { + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `).click(); + }, + message: "should wait until last seen by current partner message id changed", + predicate: ({ thread }) => { + return ( + thread.id === 10 && + thread.model === 'mail.channel' && + thread.lastSeenByCurrentPartnerMessageId === 12 + ); + }, + })); + assert.doesNotHaveClass( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `), + 'o-unread', + "sidebar item of channel ID 10 should not longer be unread" + ); +}); + +QUnit.test('receive new needaction messages', async function (assert) { + assert.expect(12); + + await this.start(); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `), + "should have inbox in sidebar" + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `).classList.contains('o-active'), + "inbox should be current discuss thread" + ); + assert.notOk( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_counter + `), + "inbox item in sidebar should not have any counter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_Message`).length, + 0, + "should have no messages in inbox initially" + ); + + // simulate receiving a new needaction message + await afterNextRender(() => { + const data = { + body: "not empty", + id: 100, + needaction_partner_ids: [3], + model: 'res.partner', + res_id: 20, + }; + const notifications = [[['my-db', 'ir.needaction', 3], data]]; + this.widget.call('bus_service', 'trigger', 'notification', notifications); + }); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_counter + `), + "inbox item in sidebar should now have counter" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_counter + `).textContent, + '1', + "inbox item in sidebar should have counter of '1'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_Message`).length, + 1, + "should have one message in inbox" + ); + assert.strictEqual( + document.querySelector(`.o_Discuss_thread .o_Message`).dataset.messageLocalId, + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId, + "should display newly received needaction message" + ); + + // simulate receiving another new needaction message + await afterNextRender(() => { + const data2 = { + body: "not empty", + id: 101, + needaction_partner_ids: [3], + model: 'res.partner', + res_id: 20, + }; + const notifications2 = [[['my-db', 'ir.needaction', 3], data2]]; + this.widget.call('bus_service', 'trigger', 'notification', notifications2); + }); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + .o_DiscussSidebarItem_counter + `).textContent, + '2', + "inbox item in sidebar should have counter of '2'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_Message`).length, + 2, + "should have 2 messages in inbox" + ); + assert.ok( + document.querySelector(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `), + "should still display 1st needaction message" + ); + assert.ok( + document.querySelector(` + .o_Discuss_thread + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 101 }).localId + }"] + `), + "should display 2nd needaction message" + ); +}); + +QUnit.test('reply to message from inbox (message linked to document)', async function (assert) { + assert.expect(19); + + // message that is expected to be found in Inbox + this.data['mail.message'].records.push({ + body: "<p>Test</p>", + date: "2019-04-20 11:00:00", + id: 100, // random unique id, will be used to link notification to message + message_type: 'comment', + // needaction needs to be set here for message_fetch domain, because + // mocked models don't have computed fields + needaction: true, + model: 'res.partner', + record_name: 'Refactoring', + res_id: 20, + }); + // notification to have message in Inbox + this.data['mail.notification'].records.push({ + mail_message_id: 100, // id of related message + res_partner_id: this.data.currentPartnerId, // must be for current partner + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_post') { + assert.step('message_post'); + assert.strictEqual( + args.model, + 'res.partner', + "should post message to record with model 'res.partner'" + ); + assert.strictEqual( + args.args[0], + 20, + "should post message to record with Id 20" + ); + assert.strictEqual( + args.kwargs.body, + "Test", + "should post with provided content in composer input" + ); + assert.strictEqual( + args.kwargs.message_type, + "comment", + "should set message type as 'comment'" + ); + } + return this._super(...arguments); + }, + }); + assert.strictEqual( + document.querySelectorAll('.o_Message').length, + 1, + "should display a single message" + ); + assert.strictEqual( + document.querySelector('.o_Message').dataset.messageLocalId, + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId, + "should display message with ID 100" + ); + assert.strictEqual( + document.querySelector('.o_Message_originThread').textContent, + " on Refactoring", + "should display message originates from record 'Refactoring'" + ); + + await afterNextRender(() => + document.querySelector('.o_Message_commandReply').click() + ); + assert.ok( + document.querySelector('.o_Message').classList.contains('o-selected'), + "message should be selected after clicking on reply icon" + ); + assert.ok( + document.querySelector('.o_Composer'), + "should have composer after clicking on reply to message" + ); + assert.strictEqual( + document.querySelector(`.o_Composer_threadName`).textContent, + " on: Refactoring", + "composer should display origin thread name of message" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ComposerTextInput_textarea`), + "composer text input should be auto-focus" + ); + + await afterNextRender(() => + document.execCommand('insertText', false, "Test") + ); + await afterNextRender(() => + document.querySelector('.o_Composer_buttonSend').click() + ); + assert.verifySteps(['message_post']); + assert.notOk( + document.querySelector('.o_Composer'), + "should no longer have composer after posting reply to message" + ); + assert.strictEqual( + document.querySelectorAll('.o_Message').length, + 1, + "should still display a single message after posting reply" + ); + assert.strictEqual( + document.querySelector('.o_Message').dataset.messageLocalId, + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId, + "should still display message with ID 100 after posting reply" + ); + assert.notOk( + document.querySelector('.o_Message').classList.contains('o-selected'), + "message should not longer be selected after posting reply" + ); + assert.ok( + document.querySelector('.o_notification'), + "should display a notification after posting reply" + ); + assert.strictEqual( + document.querySelector('.o_notification_content').textContent, + "Message posted on \"Refactoring\"", + "notification should tell that message has been posted to the record 'Refactoring'" + ); +}); + +QUnit.test('load recent messages from thread (already loaded some old messages)', async function (assert) { + assert.expect(6); + + // channel expected to be found in the sidebar, + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + for (let i = 0; i < 50; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], // id of related channel + id: 100 + i, // random unique id, will be referenced in the test + model: 'mail.channel', // expected value to link message to channel + // needaction needs to be set here for message_fetch domain, because + // mocked models don't have computed fields + needaction: i === 0, + // the goal is to have only the first (oldest) message in Inbox + needaction_partner_ids: i === 0 ? [this.data.currentPartnerId] : [], + res_id: 20, // id of related channel + }); + } + await this.start(); + assert.strictEqual( + document.querySelectorAll('.o_Message').length, + 1, + "Inbox should have a single message initially" + ); + assert.strictEqual( + document.querySelector('.o_Message').dataset.messageLocalId, + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId, + "the only message initially should be the one marked as 'needaction'" + ); + + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `).click(); + }, + message: "should wait until channel scrolled to bottom after opening it from the discuss sidebar", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector('.o_ThreadView_messageList'); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }); + assert.strictEqual( + document.querySelectorAll('.o_Message').length, + 31, + `should display 31 messages inside the channel after clicking on it (the previously known + message from Inbox and the 30 most recent messages that have been fetched)` + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `).length, + 1, + "should display the message from Inbox inside the channel too" + ); + + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => document.querySelector('.o_Discuss_thread .o_ThreadView_messageList').scrollTop = 0, + message: "should wait until channel 20 loaded more messages after scrolling to top", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'more-messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 + ); + }, + }); + assert.strictEqual( + document.querySelectorAll('.o_Message').length, + 50, + "should display 50 messages inside the channel after scrolling to load more (all messages fetched)" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 100 }).localId + }"] + `).length, + 1, + "should still display the message from Inbox inside the channel too" + ); +}); + +QUnit.test('messages marked as read move to "History" mailbox', async function (assert) { + assert.expect(10); + + // channel expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20 }); + // expected messages + this.data['mail.message'].records.push( + { + body: "not empty", + id: 100, // random unique id, useful to link notification + model: 'mail.channel', // value to link message to channel + // needaction needs to be set here for message_fetch domain, because + // mocked models don't have computed fields + needaction: true, + res_id: 20, // id of related channel + }, + { + body: "not empty", + id: 101, // random unique id, useful to link notification + model: 'mail.channel', // value to link message to channel + // needaction needs to be set here for message_fetch domain, because + // mocked models don't have computed fields + needaction: true, + res_id: 20, // id of related channel + } + ); + this.data['mail.notification'].records.push( + // notification to have first message in inbox + { + mail_message_id: 100, // id of related message + res_partner_id: this.data.currentPartnerId, // must be for current partner + }, + // notification to have second message in inbox + { + mail_message_id: 101, // id of related message + res_partner_id: this.data.currentPartnerId, // must be for current partner + } + ); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.box_history', + }, + }, + }); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `).classList.contains('o-active'), + "history mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_MessageList_empty`).length, + 1, + "should have empty thread in history" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `).click() + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `).classList.contains('o-active'), + "inbox mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_MessageList_empty`).length, + 0, + "inbox mailbox should not be empty" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_MessageList_message`).length, + 2, + "inbox mailbox should have 2 messages" + ); + + await afterNextRender(() => + document.querySelector('.o_widget_Discuss_controlPanelButtonMarkAllRead').click() + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `).classList.contains('o-active'), + "inbox mailbox should still be active after mark as read" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_MessageList_empty`).length, + 1, + "inbox mailbox should now be empty after mark as read" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `).click() + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `).classList.contains('o-active'), + "history mailbox should be active" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_MessageList_empty`).length, + 0, + "history mailbox should not be empty after mark as read" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Discuss_thread .o_MessageList_message`).length, + 2, + "history mailbox should have 2 messages" + ); +}); + +QUnit.test('mark a single message as read should only move this message to "History" mailbox', async function (assert) { + assert.expect(9); + + this.data['mail.message'].records.push( + { + body: "not empty", + id: 1, + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + }, + { + body: "not empty", + id: 2, + needaction: true, + needaction_partner_ids: [this.data.currentPartnerId], + } + ); + this.data['mail.notification'].records.push( + { + mail_message_id: 1, + res_partner_id: this.data.currentPartnerId, + }, + { + mail_message_id: 2, + res_partner_id: this.data.currentPartnerId, + } + ); + await this.start({ + discuss: { + params: { + default_active_id: 'mail.box_history', + }, + }, + }); + assert.hasClass( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `), + 'o-active', + "history mailbox should initially be the active thread" + ); + assert.containsOnce( + document.body, + '.o_MessageList_empty', + "history mailbox should initially be empty" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `).click() + ); + assert.hasClass( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.inbox.localId + }"] + `), + 'o-active', + "inbox mailbox should be active thread after clicking on it" + ); + assert.containsN( + document.body, + '.o_Message', + 2, + "inbox mailbox should have 2 messages" + ); + + await afterNextRender(() => + document.querySelector(` + .o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 1 }).localId + }"] .o_Message_commandMarkAsRead + `).click() + ); + assert.containsOnce( + document.body, + '.o_Message', + "inbox mailbox should have one less message after clicking mark as read" + ); + assert.containsOnce( + document.body, + `.o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 2 }).localId + }"]`, + "message still in inbox should be the one not marked as read" + ); + + await afterNextRender(() => + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `).click() + ); + assert.hasClass( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `), + 'o-active', + "history mailbox should be active after clicking on it" + ); + assert.containsOnce( + document.body, + '.o_Message', + "history mailbox should have only 1 message after mark as read" + ); + assert.containsOnce( + document.body, + `.o_Message[data-message-local-id="${ + this.env.models['mail.message'].findFromIdentifyingData({ id: 1 }).localId + }"]`, + "message moved in history should be the one marked as read" + ); +}); + +QUnit.test('all messages in "Inbox" in "History" after marked all as read', async function (assert) { + assert.expect(2); + + const messageOffset = 200; + for (let id = messageOffset; id < messageOffset + 40; id++) { + // message expected to be found in Inbox + this.data['mail.message'].records.push({ + body: "not empty", + id, // will be used to link notification to message + // needaction needs to be set here for message_fetch domain, because + // mocked models don't have computed fields + needaction: true, + }); + // notification to have message in Inbox + this.data['mail.notification'].records.push({ + mail_message_id: id, // id of related message + res_partner_id: this.data.currentPartnerId, // must be for current partner + }); + + } + await this.start({ + waitUntilEvent: { + eventName: 'o-component-message-list-scrolled', + message: "should wait until inbox scrolled to its last message initially", + predicate: ({ orderedMessages, scrollTop, thread }) => { + const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`); + return ( + thread && + thread.model === 'mail.box' && + thread.id === 'inbox' && + orderedMessages.length === 30 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }, + }); + + await afterNextRender(async () => { + const markAllReadButton = document.querySelector('.o_widget_Discuss_controlPanelButtonMarkAllRead'); + markAllReadButton.click(); + }); + assert.containsNone( + document.body, + '.o_Message', + "there should no message in Inbox anymore" + ); + + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + document.querySelector(` + .o_DiscussSidebarItem[data-thread-local-id="${ + this.env.messaging.history.localId + }"] + `).click(); + }, + message: "should wait until history scrolled to its last message after opening it from the discuss sidebar", + predicate: ({ orderedMessages, scrollTop, thread }) => { + const messageList = document.querySelector('.o_MessageList'); + return ( + thread && + thread.model === 'mail.box' && + thread.id === 'history' && + orderedMessages.length === 30 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }); + + // simulate a scroll to top to load more messages + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => document.querySelector('.o_MessageList').scrollTop = 0, + message: "should wait until mailbox history loaded more messages after scrolling to top", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'more-messages-loaded' && + threadViewer.thread.model === 'mail.box' && + threadViewer.thread.id === 'history' + ); + }, + }); + assert.containsN( + document.body, + '.o_Message', + 40, + "there should be 40 messages in History" + ); +}); + +QUnit.test('receive new chat message: out of odoo focus (notification, channel)', async function (assert) { + assert.expect(4); + + // channel expected to be found in the sidebar + // with a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 20, channel_type: 'chat' }); + const bus = new Bus(); + bus.on('set_title_part', null, payload => { + assert.step('set_title_part'); + assert.strictEqual(payload.part, '_chat'); + assert.strictEqual(payload.title, "1 Message"); + }); + await this.start({ + env: { bus }, + services: { + bus_service: BusService.extend({ + _beep() {}, // Do nothing + _poll() {}, // Do nothing + _registerWindowUnload() {}, // Do nothing + isOdooFocused: () => false, + updateOption() {}, + }), + }, + }); + + // simulate receiving a new message with odoo focused + await afterNextRender(() => { + const messageData = { + channel_ids: [20], + id: 126, + model: 'mail.channel', + res_id: 20, + }; + const notifications = [[['my-db', 'mail.channel', 20], messageData]]; + this.widget.call('bus_service', 'trigger', 'notification', notifications); + }); + assert.verifySteps(['set_title_part']); +}); + +QUnit.test('receive new chat message: out of odoo focus (notification, chat)', async function (assert) { + assert.expect(4); + + // chat expected to be found in the sidebar with the proper channel_type + // and a random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ channel_type: "chat", id: 10 }); + const bus = new Bus(); + bus.on('set_title_part', null, payload => { + assert.step('set_title_part'); + assert.strictEqual(payload.part, '_chat'); + assert.strictEqual(payload.title, "1 Message"); + }); + await this.start({ + env: { bus }, + services: { + bus_service: BusService.extend({ + _beep() {}, // Do nothing + _poll() {}, // Do nothing + _registerWindowUnload() {}, // Do nothing + isOdooFocused: () => false, + updateOption() {}, + }), + }, + }); + + // simulate receiving a new message with odoo focused + await afterNextRender(() => { + const messageData = { + channel_ids: [10], + id: 126, + model: 'mail.channel', + res_id: 10, + }; + const notifications = [[['my-db', 'mail.channel', 10], messageData]]; + this.widget.call('bus_service', 'trigger', 'notification', notifications); + }); + assert.verifySteps(['set_title_part']); +}); + +QUnit.test('receive new chat messages: out of odoo focus (tab title)', async function (assert) { + assert.expect(12); + + let step = 0; + // channel and chat expected to be found in the sidebar + // with random unique id and name that will be referenced in the test + this.data['mail.channel'].records.push( + { channel_type: 'chat', id: 20, public: 'private' }, + { channel_type: 'chat', id: 10, public: 'private' }, + ); + const bus = new Bus(); + bus.on('set_title_part', null, payload => { + step++; + assert.step('set_title_part'); + assert.strictEqual(payload.part, '_chat'); + if (step === 1) { + assert.strictEqual(payload.title, "1 Message"); + } + if (step === 2) { + assert.strictEqual(payload.title, "2 Messages"); + } + if (step === 3) { + assert.strictEqual(payload.title, "3 Messages"); + } + }); + await this.start({ + env: { bus }, + services: { + bus_service: BusService.extend({ + _beep() {}, // Do nothing + _poll() {}, // Do nothing + _registerWindowUnload() {}, // Do nothing + isOdooFocused: () => false, + updateOption() {}, + }), + }, + }); + + // simulate receiving a new message in chat 20 with odoo focused + await afterNextRender(() => { + const messageData1 = { + channel_ids: [20], + id: 126, + model: 'mail.channel', + res_id: 20, + }; + const notifications1 = [[['my-db', 'mail.channel', 20], messageData1]]; + this.widget.call('bus_service', 'trigger', 'notification', notifications1); + }); + assert.verifySteps(['set_title_part']); + + // simulate receiving a new message in chat 10 with odoo focused + await afterNextRender(() => { + const messageData2 = { + channel_ids: [10], + id: 127, + model: 'mail.channel', + res_id: 10, + }; + const notifications2 = [[['my-db', 'mail.channel', 10], messageData2]]; + this.widget.call('bus_service', 'trigger', 'notification', notifications2); + }); + assert.verifySteps(['set_title_part']); + + // simulate receiving another new message in chat 10 with odoo focused + await afterNextRender(() => { + const messageData3 = { + channel_ids: [10], + id: 128, + model: 'mail.channel', + res_id: 10, + }; + const notifications3 = [[['my-db', 'mail.channel', 10], messageData3]]; + this.widget.call('bus_service', 'trigger', 'notification', notifications3); + }); + assert.verifySteps(['set_title_part']); +}); + +QUnit.test('auto-focus composer on opening thread', async function (assert) { + assert.expect(14); + + // expected correspondent, with a random unique id that will be used to link + // partner to chat and a random name that will be asserted in the test + this.data['res.partner'].records.push({ id: 7, name: "Demo User" }); + this.data['mail.channel'].records.push( + // channel expected to be found in the sidebar + { + id: 20, // random unique id, will be referenced in the test + name: "General", // random name, will be asserted in the test + }, + // chat expected to be found in the sidebar + { + channel_type: 'chat', // testing a chat is the goal of the test + id: 10, // random unique id, will be referenced in the test + members: [this.data.currentPartnerId, 7], // expected partners + public: 'private', // expected value for testing a chat + } + ); + await this.start(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-name="Inbox"] + `).length, + 1, + "should have mailbox 'Inbox' in the sidebar" + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-name="Inbox"] + `).classList.contains('o-active'), + "mailbox 'Inbox' should be active initially" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-name="General"] + `).length, + 1, + "should have channel 'General' in the sidebar" + ); + assert.notOk( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-name="General"] + `).classList.contains('o-active'), + "channel 'General' should not be active initially" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebar_item[data-thread-name="Demo User"] + `).length, + 1, + "should have chat 'Demo User' in the sidebar" + ); + assert.notOk( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-name="Demo User"] + `).classList.contains('o-active'), + "chat 'Demo User' should not be active initially" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Composer`).length, + 0, + "there should be no composer when active thread of discuss is mailbox 'Inbox'" + ); + + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebar_item[data-thread-name="General"]`).click() + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-name="General"] + `).classList.contains('o-active'), + "channel 'General' should become active after selecting it from the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Composer`).length, + 1, + "there should be a composer when active thread of discuss is channel 'General'" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ComposerTextInput_textarea`), + "composer of channel 'General' should be automatically focused on opening" + ); + + document.querySelector(`.o_ComposerTextInput_textarea`).blur(); + assert.notOk( + document.activeElement === document.querySelector(`.o_ComposerTextInput_textarea`), + "composer of channel 'General' should no longer focused on click away" + ); + + await afterNextRender(() => + document.querySelector(`.o_DiscussSidebar_item[data-thread-name="Demo User"]`).click() + ); + assert.ok( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-name="Demo User"] + `).classList.contains('o-active'), + "chat 'Demo User' should become active after selecting it from the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Composer`).length, + 1, + "there should be a composer when active thread of discuss is chat 'Demo User'" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ComposerTextInput_textarea`), + "composer of chat 'Demo User' should be automatically focused on opening" + ); +}); + +QUnit.test('mark channel as seen if last message is visible when switching channels when the previous channel had a more recent last message than the current channel [REQUIRE FOCUS]', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push( + { id: 10, message_unread_counter: 1, name: 'Bla' }, + { id: 11, message_unread_counter: 1, name: 'Blu' }, + ); + this.data['mail.message'].records.push({ + body: 'oldest message', + channel_ids: [10], + id: 10, + }, { + body: 'newest message', + channel_ids: [11], + id: 11, + }); + await this.start({ + discuss: { + context: { + active_id: 'mail.channel_11', + }, + }, + waitUntilEvent: { + eventName: 'o-thread-view-hint-processed', + message: "should wait until channel 11 loaded its messages initially", + predicate: ({ hint, threadViewer }) => { + return ( + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 11 && + hint.type === 'messages-loaded' + ); + }, + }, + }); + await afterNextRender(() => this.afterEvent({ + eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', + func: () => { + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `).click(); + }, + message: "should wait until last seen by current partner message id changed", + predicate: ({ thread }) => { + return ( + thread.id === 10 && + thread.model === 'mail.channel' && + thread.lastSeenByCurrentPartnerMessageId === 10 + ); + }, + })); + assert.doesNotHaveClass( + document.querySelector(` + .o_DiscussSidebar_item[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `), + 'o-unread', + "sidebar item of channel ID 10 should no longer be unread" + ); +}); + +QUnit.test('add custom filter should filter messages accordingly to selected filter', async function (assert) { + assert.expect(4); + + this.data['mail.channel'].records.push({ + id: 20, + name: "General" + }); + await this.start({ + async mockRPC(route, args) { + if (args.method === 'message_fetch') { + const domainsAsStr = args.kwargs.domain.map(domain => domain.join('')); + assert.step(`message_fetch:${domainsAsStr.join(',')}`); + } + return this._super(...arguments); + }, + }); + assert.verifySteps(['message_fetch:needaction=true'], "A message_fetch request should have been done for needaction messages as inbox is selected by default"); + + // Open filter menu of control panel and select a custom filter (id = 0, the only one available) + await toggleFilterMenu(document.body); + await toggleAddCustomFilter(document.body); + await applyFilter(document.body); + assert.verifySteps(['message_fetch:id=0,needaction=true'], "A message_fetch request should have been done for selected filter & domain of current thread (inbox)"); +}); + +}); +}); +}); + +}); |
