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/chat_window_manager | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/chat_window_manager')
4 files changed, 2513 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager.js new file mode 100644 index 00000000..a0c49a96 --- /dev/null +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager.js @@ -0,0 +1,51 @@ +odoo.define('mail/static/src/components/chat_window_manager/chat_window_manager.js', function (require) { +'use strict'; + +const components = { + ChatWindow: require('mail/static/src/components/chat_window/chat_window.js'), + ChatWindowHiddenMenu: require('mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js'), +}; +const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { Component } = owl; + +class ChatWindowManager extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useShouldUpdateBasedOnProps(); + useStore(props => { + const chatWindowManager = this.env.messaging && this.env.messaging.chatWindowManager; + const allOrderedVisible = chatWindowManager + ? chatWindowManager.allOrderedVisible + : []; + return { + allOrderedVisible, + allOrderedVisibleThread: allOrderedVisible.map(chatWindow => chatWindow.thread), + chatWindowManager, + chatWindowManagerHasHiddenChatWindows: chatWindowManager && chatWindowManager.hasHiddenChatWindows, + isMessagingInitialized: this.env.isMessagingInitialized(), + }; + }, { + compareDepth: { + allOrderedVisible: 1, + allOrderedVisibleThread: 1, + }, + }); + } + +} + +Object.assign(ChatWindowManager, { + components, + props: {}, + template: 'mail.ChatWindowManager', +}); + +return ChatWindowManager; + +}); diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager.scss b/addons/mail/static/src/components/chat_window_manager/chat_window_manager.scss new file mode 100644 index 00000000..1e8abf54 --- /dev/null +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager.scss @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ + +.o_ChatWindowManager { + bottom: 0; + right: 0; + display: flex; + flex-direction: row-reverse; + z-index: 1000; +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager.xml b/addons/mail/static/src/components/chat_window_manager/chat_window_manager.xml new file mode 100644 index 00000000..8e2bd6bf --- /dev/null +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="mail.ChatWindowManager" owl="1"> + <div class="o_ChatWindowManager"> + <t t-if="env.isMessagingInitialized()"> + <!-- Note: DOM elements are ordered from left to right --> + <t t-if="env.messaging.chatWindowManager.hasHiddenChatWindows"> + <ChatWindowHiddenMenu class="o_ChatWindowManager_hiddenMenu"/> + </t> + <t t-foreach="env.messaging.chatWindowManager.allOrderedVisible" t-as="chatWindow" t-key="chatWindow.localId"> + <ChatWindow + chatWindowLocalId="chatWindow.localId" + hasCloseAsBackButton="env.messaging.device.isMobile" + isExpandable="!env.messaging.device.isMobile and !!chatWindow.thread" + isFullscreen="env.messaging.device.isMobile" + /> + </t> + </t> + </div> + </t> + +</templates> diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js new file mode 100644 index 00000000..ce82f2bb --- /dev/null +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js @@ -0,0 +1,2423 @@ +odoo.define('mail/static/src/components/chat_window_manager/chat_window_manager_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'); + +const { + file: { createFile, inputFiles }, + dom: { triggerEvent }, +} = require('web.test_utils'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('chat_window_manager', {}, function () { +QUnit.module('chat_window_manager_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { afterEvent, env, widget } = await start(Object.assign( + { hasChatWindow: true, hasMessagingMenu: true }, + params, + { data: this.data } + )); + this.debug = params && params.debug; + this.afterEvent = afterEvent; + this.env = env; + this.widget = widget; + }; + + /** + * Simulates the external behaviours & DOM changes implied by hiding home menu. + * Needed to assert validity of tests at technical level (actual code of home menu could not + * be used in these tests). + */ + this.hideHomeMenu = async () => { + await this.env.bus.trigger('will_hide_home_menu'); + await this.env.bus.trigger('hide_home_menu'); + }; + + /** + * Simulates the external behaviours & DOM changes implied by showing home menu. + * Needed to assert validity of tests at technical level (actual code of home menu could not + * be used in these tests). + */ + this.showHomeMenu = async () => { + await this.env.bus.trigger('will_show_home_menu'); + const $frag = document.createDocumentFragment(); + // in real condition, chat window will be removed and put in a fragment then + // reinserted into DOM + const selector = this.debug ? 'body' : '#qunit-fixture'; + $(selector).contents().appendTo($frag); + await this.env.bus.trigger('show_home_menu'); + $(selector).append($frag); + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('[technical] messaging not created', async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(2); + + const messagingBeforeCreationDeferred = makeDeferred(); + await this.start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: 'none', + }); + assert.containsOnce( + document.body, + '.o_ChatWindowManager', + "should have chat window manager even when messaging is not yet created" + ); + + // simulate messaging being created + messagingBeforeCreationDeferred.resolve(); + await nextAnimationFrame(); + + assert.containsOnce( + document.body, + '.o_ChatWindowManager', + "should still contain chat window manager after messaging has been created" + ); +}); + +QUnit.test('initial mount', async function (assert) { + assert.expect(1); + + await this.start(); + assert.containsOnce( + document.body, + '.o_ChatWindowManager', + "should have chat window manager" + ); +}); + +QUnit.test('chat window new message: basic rendering', async function (assert) { + assert.expect(10); + + await this.start(); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_newMessageButton`).click() + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header`).length, + 1, + "should have a header" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeader_name`).length, + 1, + "should have name part in header" + ); + assert.strictEqual( + document.querySelector(`.o_ChatWindow_header .o_ChatWindowHeader_name`).textContent, + "New message", + "should display 'new message' in the header" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeader_command`).length, + 1, + "should have 1 command in header" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeader_commandClose`).length, + 1, + "should have command to close chat window" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_newMessageForm`).length, + 1, + "should have a new message chat window container" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_newMessageFormLabel`).length, + 1, + "should have a part in selection with label" + ); + assert.strictEqual( + document.querySelector(`.o_ChatWindow_newMessageFormLabel`).textContent.trim(), + "To:", + "should have label 'To:' in selection" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_newMessageFormInput`).length, + 1, + "should have an input in selection" + ); +}); + +QUnit.test('chat window new message: focused on open', async function (assert) { + assert.expect(2); + + await this.start(); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_newMessageButton`).click() + ); + assert.ok( + document.querySelector(`.o_ChatWindow`).classList.contains('o-focused'), + "chat window should be focused" + ); + assert.ok( + document.activeElement, + document.querySelector(`.o_ChatWindow_newMessageFormInput`), + "chat window focused = selection input focused" + ); +}); + +QUnit.test('chat window new message: close', async function (assert) { + assert.expect(1); + + await this.start(); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_newMessageButton`).click() + ); + await afterNextRender(() => + document.querySelector(`.o_ChatWindow_header .o_ChatWindowHeader_commandClose`).click() + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 0, + "chat window should be closed" + ); +}); + +QUnit.test('chat window new message: fold', async function (assert) { + assert.expect(6); + + await this.start(); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_newMessageButton`).click() + ); + assert.doesNotHaveClass( + document.querySelector(`.o_ChatWindow`), + 'o-folded', + "chat window should not be folded by default" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow_newMessageForm', + "chat window should have new message form" + ); + + await afterNextRender(() => document.querySelector(`.o_ChatWindow_header`).click()); + assert.hasClass( + document.querySelector(`.o_ChatWindow`), + 'o-folded', + "chat window should become folded" + ); + assert.containsNone( + document.body, + '.o_ChatWindow_newMessageForm', + "chat window should not have new message form" + ); + + await afterNextRender(() => document.querySelector(`.o_ChatWindow_header`).click()); + assert.doesNotHaveClass( + document.querySelector(`.o_ChatWindow`), + 'o-folded', + "chat window should become unfolded" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow_newMessageForm', + "chat window should have new message form" + ); +}); + +QUnit.test('open chat from "new message" chat window should open chat in place of this "new message" chat window', async function (assert) { + /** + * InnerWith computation uses following info: + * ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager/chat_window_manager.js:visual) + * + * - chat window width: 325px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 200px + * - global width: 1920px + * + * Enough space for 3 visible chat windows: + * 10 + 325 + 5 + 325 + 5 + 325 + 10 = 1000 < 1920 + */ + assert.expect(11); + + this.data['res.partner'].records.push({ id: 131, name: "Partner 131" }); + this.data['res.users'].records.push({ partner_id: 131 }); + this.data['mail.channel'].records.push( + { is_minimized: true }, + { is_minimized: true }, + ); + const imSearchDef = makeDeferred(); + await this.start({ + env: { + browser: { + innerWidth: 1920, + }, + }, + async mockRPC(route, args) { + const res = await this._super(...arguments); + if (args.method === 'im_search') { + imSearchDef.resolve(); + } + return res; + } + }); + assert.containsN( + document.body, + '.o_ChatWindow', + 2, + "should have 2 chat windows initially" + ); + assert.containsNone( + document.body, + '.o_ChatWindow.o-new-message', + "should not have any 'new message' chat window initially" + ); + + // open "new message" chat window + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_newMessageButton`).click() + ); + assert.containsOnce( + document.body, + '.o_ChatWindow.o-new-message', + "should have 'new message' chat window after clicking 'new message' in messaging menu" + ); + assert.containsN( + document.body, + '.o_ChatWindow', + 3, + "should have 3 chat window after opening 'new message' chat window", + ); + assert.containsOnce( + document.body, + '.o_ChatWindow_newMessageFormInput', + "'new message' chat window should have new message form input" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="2"]'), + 'o-new-message', + "'new message' chat window should be the last chat window initially", + ); + + await afterNextRender(() => + document.querySelector('.o_ChatWindow[data-visible-index="2"] .o_ChatWindowHeader_commandShiftRight').click() + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="1"]'), + 'o-new-message', + "'new message' chat window should have moved to the middle after clicking shift previous", + ); + + // search for a user in "new message" autocomplete + document.execCommand('insertText', false, "131"); + document.querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + // Wait for search RPC to be resolved. The following await lines are + // necessary because autocomplete is an external lib therefore it is not + // possible to use `afterNextRender`. + await imSearchDef; + await nextAnimationFrame(); + const link = document.querySelector('.ui-autocomplete .ui-menu-item a'); + assert.ok( + link, + "should have autocomplete suggestion after typing on 'new message' input" + ); + assert.strictEqual( + link.textContent, + "Partner 131", + "autocomplete suggestion should target the partner matching search term" + ); + + await afterNextRender(() => link.click()); + assert.containsNone( + document.body, + '.o_ChatWindow.o-new-message', + "should have removed the 'new message' chat window after selecting a partner" + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="1"] .o_ChatWindowHeader_name').textContent, + "Partner 131", + "chat window with selected partner should be opened in position where 'new message' chat window was, which is in the middle" + ); +}); + +QUnit.test('new message chat window should close on selecting the user if chat with the user is already open', async function (assert) { + assert.expect(2); + + this.data['res.partner'].records.push({ id: 131, name: "Partner 131"}); + this.data['res.users'].records.push({ id: 12, partner_id: 131 }); + this.data['mail.channel'].records.push({ + channel_type: "chat", + id: 20, + is_minimized: true, + members: [this.data.currentPartnerId, 131], + name: "Partner 131", + public: 'private', + state: 'open', + }); + const imSearchDef = makeDeferred(); + await this.start({ + async mockRPC(route, args) { + const res = await this._super(...arguments); + if (args.method === 'im_search') { + imSearchDef.resolve(); + } + return res; + }, + }); + + // open "new message" chat window + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_newMessageButton`).click()); + + // search for a user in "new message" autocomplete + document.execCommand('insertText', false, "131"); + document.querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + // Wait for search RPC to be resolved. The following await lines are + // necessary because autocomplete is an external lib therefore it is not + // possible to use `afterNextRender`. + await imSearchDef; + await nextAnimationFrame(); + const link = document.querySelector('.ui-autocomplete .ui-menu-item a'); + + await afterNextRender(() => link.click()); + assert.containsNone( + document.body, + '.o_ChatWindow_newMessageFormInput', + "'new message' chat window should not be there" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "should have only one chat window after selecting user whose chat is already open", + ); +}); + +QUnit.test('new message autocomplete should automatically select first result', async function (assert) { + assert.expect(1); + + this.data['res.partner'].records.push({ id: 131, name: "Partner 131" }); + this.data['res.users'].records.push({ partner_id: 131 }); + const imSearchDef = makeDeferred(); + await this.start({ + async mockRPC(route, args) { + const res = await this._super(...arguments); + if (args.method === 'im_search') { + imSearchDef.resolve(); + } + return res; + }, + }); + + // open "new message" chat window + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_newMessageButton`).click() + ); + + // search for a user in "new message" autocomplete + document.execCommand('insertText', false, "131"); + document.querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + // Wait for search RPC to be resolved. The following await lines are + // necessary because autocomplete is an external lib therefore it is not + // possible to use `afterNextRender`. + await imSearchDef; + await nextAnimationFrame(); + assert.hasClass( + document.querySelector('.ui-autocomplete .ui-menu-item a'), + 'ui-state-active', + "first autocomplete result should be automatically selected", + ); +}); + +QUnit.test('chat window: basic rendering', async function (assert) { + assert.expect(11); + + // channel that is expected to be found in the messaging menu + // with random unique id and name that will be asserted during the test + this.data['mail.channel'].records.push({ id: 20, name: "General" }); + await this.start(); + + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(`.o_NotificationList_preview`).click() + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + const chatWindow = document.querySelector(`.o_ChatWindow`); + assert.strictEqual( + chatWindow.dataset.threadLocalId, + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId, + "should have open a chat window of channel" + ); + assert.strictEqual( + chatWindow.querySelectorAll(`:scope .o_ChatWindow_header`).length, + 1, + "should have header part" + ); + const chatWindowHeader = chatWindow.querySelector(`:scope .o_ChatWindow_header`); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ThreadIcon`).length, + 1, + "should have thread icon in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeader_name`).length, + 1, + "should have thread name in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelector(`:scope .o_ChatWindowHeader_name`).textContent, + "General", + "should have correct thread name in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeader_command`).length, + 2, + "should have 2 commands in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeader_commandExpand`).length, + 1, + "should have command to expand thread in discuss" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeader_commandClose`).length, + 1, + "should have command to close chat window" + ); + assert.strictEqual( + chatWindow.querySelectorAll(`:scope .o_ChatWindow_thread`).length, + 1, + "should have part to display thread content inside chat window" + ); + assert.ok( + chatWindow.querySelector(`:scope .o_ChatWindow_thread`).classList.contains('o_ThreadView'), + "thread part should use component ThreadView" + ); +}); + +QUnit.test('chat window: fold', async function (assert) { + assert.expect(9); + + // channel that is expected to be found in the messaging menu + // with random UUID, will be asserted during the test + this.data['mail.channel'].records.push({ uuid: 'channel-uuid' }); + await this.start({ + mockRPC(route, args) { + if (args.method === 'channel_fold') { + assert.step(`rpc:${args.method}/${args.kwargs.state}`); + } + return this._super(...arguments); + }, + }); + // Open Thread + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click() + ); + assert.containsOnce( + document.body, + '.o_ChatWindow_thread', + "chat window should have a thread" + ); + assert.verifySteps( + ['rpc:channel_fold/open'], + "should sync fold state 'open' with server after opening chat window" + ); + + // Fold chat window + await afterNextRender(() => document.querySelector(`.o_ChatWindow_header`).click()); + assert.verifySteps( + ['rpc:channel_fold/folded'], + "should sync fold state 'folded' with server after folding chat window" + ); + assert.containsNone( + document.body, + '.o_ChatWindow_thread', + "chat window should not have any thread" + ); + + // Unfold chat window + await afterNextRender(() => document.querySelector(`.o_ChatWindow_header`).click()); + assert.verifySteps( + ['rpc:channel_fold/open'], + "should sync fold state 'open' with server after unfolding chat window" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow_thread', + "chat window should have a thread" + ); +}); + +QUnit.test('chat window: open / close', async function (assert) { + assert.expect(10); + + // channel that is expected to be found in the messaging menu + // with random UUID, will be asserted during the test + this.data['mail.channel'].records.push({ uuid: 'channel-uuid' }); + await this.start({ + mockRPC(route, args) { + if (args.method === 'channel_fold') { + assert.step(`rpc:channel_fold/${args.kwargs.state}`); + } + return this._super(...arguments); + }, + }); + assert.containsNone( + document.body, + '.o_ChatWindow', + "should not have a chat window initially" + ); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click() + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "should have a chat window after clicking on thread preview" + ); + assert.verifySteps( + ['rpc:channel_fold/open'], + "should sync fold state 'open' with server after opening chat window" + ); + + // Close chat window + await afterNextRender(() => document.querySelector(`.o_ChatWindowHeader_commandClose`).click()); + assert.containsNone( + document.body, + '.o_ChatWindow', + "should not have a chat window after closing it" + ); + assert.verifySteps( + ['rpc:channel_fold/closed'], + "should sync fold state 'closed' with server after closing chat window" + ); + + // Reopen chat window + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click() + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "should have a chat window again after clicking on thread preview again" + ); + assert.verifySteps( + ['rpc:channel_fold/open'], + "should sync fold state 'open' with server after opening chat window again" + ); +}); + +QUnit.test('Mobile: opening a chat window should not update channel state on the server', async function (assert) { + assert.expect(2); + + this.data['mail.channel'].records.push({ + id: 20, + state: 'closed', + }); + await this.start({ + env: { + device: { + isMobile: true, + }, + }, + }); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => document.querySelector(`.o_NotificationList_preview`).click()); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "should have a chat window after clicking on thread preview" + ); + const channels = await this.env.services.rpc({ + model: 'mail.channel', + method: 'read', + args: [20], + }, { shadow: true }); + assert.strictEqual( + channels[0].state, + 'closed', + 'opening a chat window in mobile should not update channel state on the server', + ); +}); + +QUnit.test('Mobile: closing a chat window should not update channel state on the server', async function (assert) { + assert.expect(3); + + this.data['mail.channel'].records.push({ + id: 20, + state: 'open', + }); + await this.start({ + env: { + device: { + isMobile: true, + }, + }, + }); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => document.querySelector(`.o_NotificationList_preview`).click()); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "should have a chat window after clicking on thread preview" + ); + // Close chat window + await afterNextRender(() => document.querySelector(`.o_ChatWindowHeader_commandClose`).click()); + assert.containsNone( + document.body, + '.o_ChatWindow', + "should not have a chat window after closing it" + ); + const channels = await this.env.services.rpc({ + model: 'mail.channel', + method: 'read', + args: [20], + }, { shadow: true }); + assert.strictEqual( + channels[0].state, + 'open', + 'closing the chat window should not update channel state on the server', + ); +}); + +QUnit.test("Mobile: chat window shouldn't open automatically after receiving a new message", async function (assert) { + assert.expect(1); + + this.data['res.partner'].records.push({ id: 10, name: "Demo" }); + this.data['res.users'].records.push({ + id: 42, + partner_id: 10, + }); + this.data['mail.channel'].records = [ + { + channel_type: "chat", + id: 10, + members: [this.data.currentPartnerId, 10], + uuid: 'channel-10-uuid', + }, + ]; + await this.start({ + env: { + device: { + isMobile: true, + }, + }, + }); + + // simulate receiving a message + await afterNextRender(() => this.env.services.rpc({ + route: '/mail/chat_post', + params: { + context: { + mockedUserId: 42, + }, + message_content: "hu", + uuid: 'channel-10-uuid', + }, + })); + assert.containsNone( + document.body, + '.o_ChatWindow', + "On mobile, the chat window shouldn't open automatically after receiving a new message" + ); +}); + +QUnit.test('chat window: close on ESCAPE', async function (assert) { + assert.expect(10); + + // expected partner to be found by mention during the test + this.data['res.partner'].records.push({ name: "TestPartner" }); + // a chat window with thread is expected to be initially open for this test + this.data['mail.channel'].records.push({ is_minimized: true }); + await this.start({ + mockRPC(route, args) { + if (args.method === 'channel_fold') { + assert.step(`rpc:channel_fold/${args.kwargs.state}`); + } + return this._super(...arguments); + }, + }); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "chat window should be opened initially" + ); + + 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_ChatWindow', + "chat window 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')); + }); + assert.hasClass( + document.querySelector('.o_ComposerSuggestionList_list'), + 'show', + "should display mention suggestions on 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_ComposerSuggestionList_list', + "mention suggestion should be closed after pressing escape on mention suggestion" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "chat window 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_ChatWindow', + "chat window should be closed after pressing escape if there was no other priority escape handler" + ); + assert.verifySteps(['rpc:channel_fold/closed']); +}); + +QUnit.test('focus next visible chat window when closing current chat window with ESCAPE', async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager/chat_window_manager.js:visual) + * + * - chat window width: 325px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 200px + * - global width: 1920px + * + * Enough space for 2 visible chat windows: + * 10 + 325 + 5 + 325 + 10 = 670 < 1920 + */ + assert.expect(4); + + // 2 chat windows with thread are expected to be initially open for this test + this.data['mail.channel'].records.push( + { is_minimized: true, state: 'open' }, + { is_minimized: true, state: 'open' } + ); + await this.start({ + env: { + browser: { + innerWidth: 1920, + }, + }, + }); + assert.containsN( + document.body, + '.o_ChatWindow .o_ComposerTextInput_textarea', + 2, + "2 chat windows should be present initially" + ); + assert.containsNone( + document.body, + '.o_ChatWindow.o-folded', + "both chat windows should be open" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }); + document.querySelector('.o_ComposerTextInput_textarea').dispatchEvent(ev); + }); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "only one chat window should remain after pressing escape on first chat window" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow'), + 'o-focused', + "next visible chat window should be focused after pressing escape on first chat window" + ); +}); + +QUnit.test('[technical] chat window: composer state conservation on toggle home menu', async function (assert) { + // technical as show/hide home menu simulation are involved and home menu implementation + // have side-effects on DOM that may make chat window components not work + assert.expect(7); + + // channel that is expected to be found in the messaging menu + // with random unique id that is needed to link messages + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click() + ); + // Set content of the composer of the chat window + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, 'XDU for the win !'); + }); + assert.containsNone( + document.body, + '.o_Composer .o_Attachment', + "composer should have no attachment initially" + ); + // Set attachments of the composer + const files = [ + await createFile({ + name: 'text state conservation on toggle home menu.txt', + content: 'hello, world', + contentType: 'text/plain', + }), + await createFile({ + name: 'text2 state conservation on toggle home menu.txt', + content: 'hello, xdu is da best man', + contentType: 'text/plain', + }) + ]; + await afterNextRender(() => + inputFiles( + document.querySelector('.o_FileUploader_input'), + files + ) + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "XDU for the win !", + "chat window composer initial text input should contain 'XDU for the win !'" + ); + assert.containsN( + document.body, + '.o_Composer .o_Attachment', + 2, + "composer should have 2 total attachments after adding 2 attachments" + ); + + await this.hideHomeMenu(); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "XDU for the win !", + "Chat window composer should still have the same input after hiding home menu" + ); + assert.containsN( + document.body, + '.o_Composer .o_Attachment', + 2, + "Chat window composer should have 2 attachments after hiding home menu" + ); + + // Show home menu + await this.showHomeMenu(); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "XDU for the win !", + "chat window composer should still have the same input showing home menu" + ); + assert.containsN( + document.body, + '.o_Composer .o_Attachment', + 2, + "Chat window composer should have 2 attachments showing home menu" + ); +}); + +QUnit.test('[technical] chat window: scroll conservation on toggle home menu', async function (assert) { + // technical as show/hide home menu simulation are involved and home menu implementation + // have side-effects on DOM that may make chat window components not work + assert.expect(2); + + this.data['mail.channel'].records.push({ id: 20 }); + for (let i = 0; i < 10; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + }); + } + await this.start(); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => document.querySelector('.o_NotificationList_preview').click(), + message: "should wait until channel 20 scrolled to its last message after opening it from the messaging menu", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector('.o_ThreadView_messageList'); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }); + // Set a scroll position to chat window + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; + }, + message: "should wait until channel 20 scrolled to 142 after setting this value manually", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === 142 + ); + }, + }); + await afterNextRender(() => this.hideHomeMenu()); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same after home menu is hidden" + ); + + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => this.showHomeMenu(), + message: "should wait until channel 20 restored its scroll to 142 after showing the home menu", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same after home menu is shown" + ); +}); + +QUnit.test('open 2 different chat windows: enough screen width [REQUIRE FOCUS]', async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager/chat_window_manager.js:visual) + * + * - chat window width: 325px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 200px + * - global width: 1920px + * + * Enough space for 2 visible chat windows: + * 10 + 325 + 5 + 325 + 10 = 670 < 1920 + */ + assert.expect(8); + + // 2 channels are expected to be found in the messaging menu, each with a + // random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 10 }, { id: 20 }); + await this.start({ + env: { + browser: { + innerWidth: 1920, // enough to fit at least 2 chat windows + }, + }, + }); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => + document.querySelector(` + .o_MessagingMenu_dropdownMenu + .o_NotificationList_preview[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `).click() + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "chat window of chat should be open" + ); + assert.ok( + document.querySelector(` + .o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `).classList.contains('o-focused'), + "chat window of chat should have focus" + ); + + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => + document.querySelector(` + .o_MessagingMenu_dropdownMenu + .o_NotificationList_preview[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `).click() + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 2, + "should have open a new chat window" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "chat window of channel should be open" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "chat window of chat should still be open" + ); + assert.ok( + document.querySelector(` + .o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }).localId + }"] + `).classList.contains('o-focused'), + "chat window of channel should have focus" + ); + assert.notOk( + document.querySelector(` + .o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 10, + model: 'mail.channel', + }).localId + }"] + `).classList.contains('o-focused'), + "chat window of chat should no longer have focus" + ); +}); + +QUnit.test('open 2 chat windows: check shift operations are available', async function (assert) { + assert.expect(9); + + // 2 channels are expected to be found in the messaging menu + // only their existence matters, data are irrelevant + this.data['mail.channel'].records.push({}, {}); + await this.start(); + + await afterNextRender(() => { + document.querySelector('.o_MessagingMenu_toggler').click(); + }); + await afterNextRender(() => { + document.querySelectorAll('.o_MessagingMenu_dropdownMenu .o_NotificationList_preview')[0].click(); + }); + await afterNextRender(() => { + document.querySelector('.o_MessagingMenu_toggler').click(); + }); + await afterNextRender(() => { + document.querySelectorAll('.o_MessagingMenu_dropdownMenu .o_NotificationList_preview')[1].click(); + }); + assert.containsN( + document.body, + '.o_ChatWindow', + 2, + "should have opened 2 chat windows" + ); + assert.containsOnce( + document.querySelectorAll('.o_ChatWindow')[0], + '.o_ChatWindowHeader_commandShiftLeft', + "first chat window should be allowed to shift left" + ); + assert.containsNone( + document.querySelectorAll('.o_ChatWindow')[0], + '.o_ChatWindowHeader_commandShiftRight', + "first chat window should not be allowed to shift right" + ); + assert.containsNone( + document.querySelectorAll('.o_ChatWindow')[1], + '.o_ChatWindowHeader_commandShiftLeft', + "second chat window should not be allowed to shift left" + ); + assert.containsOnce( + document.querySelectorAll('.o_ChatWindow')[1], + '.o_ChatWindowHeader_commandShiftRight', + "second chat window should be allowed to shift right" + ); + + const initialFirstChatWindowThreadLocalId = + document.querySelectorAll('.o_ChatWindow')[0].dataset.threadLocalId; + const initialSecondChatWindowThreadLocalId = + document.querySelectorAll('.o_ChatWindow')[1].dataset.threadLocalId; + await afterNextRender(() => { + document.querySelectorAll('.o_ChatWindow')[0] + .querySelector(':scope .o_ChatWindowHeader_commandShiftLeft') + .click(); + }); + assert.strictEqual( + document.querySelectorAll('.o_ChatWindow')[0].dataset.threadLocalId, + initialSecondChatWindowThreadLocalId, + "First chat window should be second after it has been shift left" + ); + assert.strictEqual( + document.querySelectorAll('.o_ChatWindow')[1].dataset.threadLocalId, + initialFirstChatWindowThreadLocalId, + "Second chat window should be first after the first has been shifted left" + ); + + await afterNextRender(() => { + document.querySelectorAll('.o_ChatWindow')[1] + .querySelector(':scope .o_ChatWindowHeader_commandShiftRight') + .click(); + }); + assert.strictEqual( + document.querySelectorAll('.o_ChatWindow')[0].dataset.threadLocalId, + initialFirstChatWindowThreadLocalId, + "First chat window should be back at first place after being shifted left then right" + ); + assert.strictEqual( + document.querySelectorAll('.o_ChatWindow')[1].dataset.threadLocalId, + initialSecondChatWindowThreadLocalId, + "Second chat window should be back at second place after first one has been shifted left then right" + ); +}); + +QUnit.test('open 2 folded chat windows: check shift operations are available', async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: 900px) + * (others: @see `mail/static/src/models/chat_window_manager/chat_window_manager.js:visual`) + * + * - chat window width: 325px + * - start/end/between gap width: 10px/10px/5px + * - global width: 900px + * + * 2 visible chat windows + hidden menu: + * 10 + 325 + 5 + 325 + 10 = 675 < 900 + */ + assert.expect(13); + + this.data['res.partner'].records.push({ id: 7, name: "Demo" }); + const channel = { + channel_type: "channel", + is_minimized: true, + is_pinned: true, + state: 'folded', + }; + const chat = { + channel_type: "chat", + is_minimized: true, + is_pinned: true, + members: [this.data.currentPartnerId, 7], + state: 'folded', + }; + this.data['mail.channel'].records.push(channel, chat); + await this.start({ + env: { + browser: { + innerWidth: 900, + }, + }, + }); + + assert.containsN( + document.body, + '.o_ChatWindow', + 2, + "should have opened 2 chat windows initially" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="0"]'), + 'o-folded', + "first chat window should be folded" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="1"]'), + 'o-folded', + "second chat window should be folded" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow .o_ChatWindowHeader_commandShiftLeft', + "there should be only one chat window allowed to shift left even if folded" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow .o_ChatWindowHeader_commandShiftRight', + "there should be only one chat window allowed to shift right even if folded" + ); + + const initialFirstChatWindowThreadLocalId = + document.querySelector('.o_ChatWindow[data-visible-index="0"]').dataset.threadLocalId; + const initialSecondChatWindowThreadLocalId = + document.querySelector('.o_ChatWindow[data-visible-index="1"]').dataset.threadLocalId; + await afterNextRender(() => + document.querySelector('.o_ChatWindowHeader_commandShiftLeft').click() + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="0"]').dataset.threadLocalId, + initialSecondChatWindowThreadLocalId, + "First chat window should be second after it has been shift left" + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="1"]').dataset.threadLocalId, + initialFirstChatWindowThreadLocalId, + "Second chat window should be first after the first has been shifted left" + ); + + await afterNextRender(() => + document.querySelector('.o_ChatWindowHeader_commandShiftLeft').click() + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="0"]').dataset.threadLocalId, + initialFirstChatWindowThreadLocalId, + "First chat window should be back at first place" + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="1"]').dataset.threadLocalId, + initialSecondChatWindowThreadLocalId, + "Second chat window should be back at second place" + ); + + await afterNextRender(() => + document.querySelector('.o_ChatWindowHeader_commandShiftRight').click() + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="0"]').dataset.threadLocalId, + initialSecondChatWindowThreadLocalId, + "First chat window should be second after it has been shift right" + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="1"]').dataset.threadLocalId, + initialFirstChatWindowThreadLocalId, + "Second chat window should be first after the first has been shifted right" + ); + + await afterNextRender(() => + document.querySelector('.o_ChatWindowHeader_commandShiftRight').click() + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="0"]').dataset.threadLocalId, + initialFirstChatWindowThreadLocalId, + "First chat window should be back at first place" + ); + assert.strictEqual( + document.querySelector('.o_ChatWindow[data-visible-index="1"]').dataset.threadLocalId, + initialSecondChatWindowThreadLocalId, + "Second chat window should be back at second place" + ); +}); + +QUnit.test('open 3 different chat windows: not enough screen width', async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: 900px) + * (others: @see `mail/static/src/models/chat_window_manager/chat_window_manager.js:visual`) + * + * - chat window width: 325px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 200px + * - global width: 1080px + * + * Enough space for 2 visible chat windows, and one hidden chat window: + * 3 visible chat windows: + * 10 + 325 + 5 + 325 + 5 + 325 + 10 = 1000 < 900 + * 2 visible chat windows + hidden menu: + * 10 + 325 + 5 + 325 + 10 + 200 + 5 = 875 < 900 + */ + assert.expect(12); + + // 3 channels are expected to be found in the messaging menu, each with a + // random unique id that will be referenced in the test + this.data['mail.channel'].records.push({ id: 1 }, { id: 2 }, { id: 3 }); + await this.start({ + env: { + browser: { + innerWidth: 900, // enough to fit 2 chat windows but not 3 + }, + }, + }); + + // open, from systray menu, chat windows of channels with Id 1, 2, then 3 + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(` + .o_MessagingMenu_dropdownMenu + .o_NotificationList_preview[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 1, + model: 'mail.channel', + }).localId + }"] + `).click() + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open 1 visible chat window" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, + 0, + "should not have hidden menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "messaging menu should be hidden" + ); + + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(` + .o_MessagingMenu_dropdownMenu + .o_NotificationList_preview[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 2, + model: 'mail.channel', + }).localId + }"] + `).click() + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 2, + "should have open 2 visible chat windows" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, + 0, + "should not have hidden menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "messaging menu should be hidden" + ); + + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(` + .o_MessagingMenu_dropdownMenu + .o_NotificationList_preview[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 3, + model: 'mail.channel', + }).localId + }"] + `).click() + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 2, + "should have open 2 visible chat windows" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, + 1, + "should have hidden menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "messaging menu should be hidden" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 1, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "chat window of channel 1 should be open" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 3, + model: 'mail.channel', + }).localId + }"] + `).length, + 1, + "chat window of channel 3 should be open" + ); + assert.ok( + document.querySelector(` + .o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 3, + model: 'mail.channel', + }).localId + }"] + `).classList.contains('o-focused'), + "chat window of channel 3 should have focus" + ); +}); + +QUnit.test('chat window: switch on TAB', async function (assert) { + assert.expect(10); + + // 2 channels are expected to be found in the messaging menu + // with random unique id and name that will be asserted during the test + this.data['mail.channel'].records.push( + { id: 1, name: "channel1" }, + { id: 2, name: "channel2" } + ); + await this.start(); + + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(` + .o_MessagingMenu_dropdownMenu + .o_NotificationList_preview[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 1, + model: 'mail.channel', + }).localId + }"]` + ).click() + ); + + assert.containsOnce(document.body, '.o_ChatWindow', "Only 1 chatWindow must be opened"); + const chatWindow = document.querySelector('.o_ChatWindow'); + assert.strictEqual( + chatWindow.querySelector('.o_ChatWindowHeader_name').textContent, + 'channel1', + "The name of the only chatWindow should be 'channel1' (channel with ID 1)" + ); + assert.strictEqual( + chatWindow.querySelector('.o_ComposerTextInput_textarea'), + document.activeElement, + "The chatWindow composer must have focus" + ); + + await afterNextRender(() => + triggerEvent( + chatWindow.querySelector('.o_ChatWindow .o_ComposerTextInput_textarea'), + 'keydown', + { key: 'Tab' }, + ) + ); + assert.strictEqual( + chatWindow.querySelector('.o_ChatWindow .o_ComposerTextInput_textarea'), + document.activeElement, + "The chatWindow composer still has focus" + ); + + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_toggler`).click() + ); + await afterNextRender(() => + document.querySelector(` + .o_MessagingMenu_dropdownMenu + .o_NotificationList_preview[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 2, + model: 'mail.channel', + }).localId + }"]` + ).click() + ); + + assert.containsN(document.body, '.o_ChatWindow', 2, "2 chatWindows must be opened"); + const chatWindows = document.querySelectorAll('.o_ChatWindow'); + assert.strictEqual( + chatWindows[0].querySelector('.o_ChatWindowHeader_name').textContent, + 'channel1', + "The name of the 1st chatWindow should be 'channel1' (channel with ID 1)" + ); + assert.strictEqual( + chatWindows[1].querySelector('.o_ChatWindowHeader_name').textContent, + 'channel2', + "The name of the 2nd chatWindow should be 'channel2' (channel with ID 2)" + ); + assert.strictEqual( + chatWindows[1].querySelector('.o_ComposerTextInput_textarea'), + document.activeElement, + "The 2nd chatWindow composer must have focus (channel with ID 2)" + ); + + await afterNextRender(() => + triggerEvent( + chatWindows[1].querySelector('.o_ComposerTextInput_textarea'), + 'keydown', + { key: 'Tab' }, + ) + ); + assert.containsN(document.body, '.o_ChatWindow', 2, "2 chatWindows should still be opened"); + assert.strictEqual( + chatWindows[0].querySelector('.o_ComposerTextInput_textarea'), + document.activeElement, + "The 1st chatWindow composer must have focus (channel with ID 1)" + ); +}); + +QUnit.test('chat window: TAB cycle with 3 open chat windows [REQUIRE FOCUS]', async function (assert) { + /** + * InnerWith computation uses following info: + * ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager/chat_window_manager.js:visual) + * + * - chat window width: 325px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 200px + * - global width: 1920px + * + * Enough space for 3 visible chat windows: + * 10 + 325 + 5 + 325 + 5 + 325 + 10 = 1000 < 1920 + */ + assert.expect(6); + + this.data['mail.channel'].records.push( + { + is_minimized: true, + is_pinned: true, + state: 'open', + }, + { + is_minimized: true, + is_pinned: true, + state: 'open', + }, + { + is_minimized: true, + is_pinned: true, + state: 'open', + } + ); + await this.start({ + env: { + browser: { + innerWidth: 1920, + }, + }, + }); + assert.containsN( + document.body, + '.o_ChatWindow .o_ComposerTextInput_textarea', + 3, + "initialy, 3 chat windows should be present" + ); + assert.containsNone( + document.body, + '.o_ChatWindow.o-folded', + "all 3 chat windows should be open" + ); + + await afterNextRender(() => { + document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInput_textarea").focus(); + }); + assert.strictEqual( + document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInput_textarea"), + document.activeElement, + "The chatWindow with visible-index 2 should have the focus" + ); + + await afterNextRender(() => + triggerEvent( + document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInput_textarea"), + 'keydown', + { key: 'Tab' }, + ) + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInput_textarea"), + document.activeElement, + "after pressing tab on the chatWindow with visible-index 2, the chatWindow with visible-index 1 should have focus" + ); + + await afterNextRender(() => + triggerEvent( + document.querySelector(".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInput_textarea"), + 'keydown', + { key: 'Tab' }, + ) + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInput_textarea"), + document.activeElement, + "after pressing tab on the chat window with visible-index 1, the chatWindow with visible-index 0 should have focus" + ); + + await afterNextRender(() => + triggerEvent( + document.querySelector(".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInput_textarea"), + 'keydown', + { key: 'Tab' }, + ) + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInput_textarea"), + document.activeElement, + "the chatWindow with visible-index 2 should have the focus after pressing tab on the chatWindow with visible-index 0" + ); +}); + +QUnit.test('chat window with a thread: keep scroll position in message list on folded', async function (assert) { + assert.expect(3); + + // channel that is expected to be found in the messaging menu + // with a random unique id, needed to link messages + this.data['mail.channel'].records.push({ id: 20 }); + for (let i = 0; i < 10; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + }); + } + await this.start(); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => document.querySelector('.o_NotificationList_preview').click(), + message: "should wait until channel 20 scrolled to its last message after opening it from the messaging menu", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector('.o_ThreadView_messageList'); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }); + // Set a scroll position to chat window + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; + }, + message: "should wait until channel 20 scrolled to 142 after setting this value manually", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "verify chat window initial scrollTop" + ); + + // fold chat window + await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click()); + assert.containsNone( + document.body, + ".o_ThreadView", + "chat window should be folded so no ThreadView should be present" + ); + + // unfold chat window + await afterNextRender(() => this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => document.querySelector('.o_ChatWindow_header').click(), + message: "should wait until channel 20 restored its scroll position to 142", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === 142 + + ); + }, + })); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same when chat window is unfolded" + ); +}); + +QUnit.test('chat window should scroll to the newly posted message just after posting it', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ + id: 20, + is_minimized: true, + state: 'open', + }); + for (let i = 0; i < 10; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + }); + } + await this.start(); + + // Set content of the composer of the chat window + await afterNextRender(() => { + document.querySelector('.o_ComposerTextInput_textarea').focus(); + document.execCommand('insertText', false, 'WOLOLO'); + }); + // Send a new message in the chatwindow to trigger the scroll + await afterNextRender(() => + triggerEvent( + document.querySelector('.o_ChatWindow .o_ComposerTextInput_textarea'), + 'keydown', + { key: 'Enter' }, + ) + ); + const messageList = document.querySelector('.o_MessageList'); + assert.strictEqual( + messageList.scrollHeight - messageList.scrollTop, + messageList.clientHeight, + "chat window should scroll to the newly posted message just after posting it" + ); +}); + +QUnit.test('chat window: post message on non-mailing channel with "CTRL-Enter" keyboard shortcut for small screen size', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ + id: 20, + is_minimized: true, + mass_mailing: false, + }); + await this.start({ + env: { + device: { + isMobile: true, // here isMobile is used for the small screen size, not actually for the mobile devices + }, + }, + }); + + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click() + ); + // 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 for small screen" + ); +}); + +QUnit.test('[technical] chat window: composer state conservation on toggle home menu when folded', async function (assert) { + // technical as show/hide home menu simulation are involved and home menu implementation + // have side-effects on DOM that may make chat window components not work + assert.expect(6); + + // channel that is expected to be found in the messaging menu + // only its existence matters, data are irrelevant + this.data['mail.channel'].records.push({}); + await this.start(); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await afterNextRender(() => + document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click() + ); + // Set content of the composer of the chat window + await afterNextRender(() => { + document.querySelector(`.o_ComposerTextInput_textarea`).focus(); + document.execCommand('insertText', false, 'XDU for the win !'); + }); + // Set attachments of the composer + const files = [ + await createFile({ + name: 'text state conservation on toggle home menu.txt', + content: 'hello, world', + contentType: 'text/plain', + }), + await createFile({ + name: 'text2 state conservation on toggle home menu.txt', + content: 'hello, xdu is da best man', + contentType: 'text/plain', + }) + ]; + await afterNextRender(() => + inputFiles( + document.querySelector('.o_FileUploader_input'), + files + ) + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "XDU for the win !", + "verify chat window composer initial html input" + ); + assert.containsN( + document.body, + '.o_Composer .o_Attachment', + 2, + "verify chat window composer initial attachment count" + ); + + // fold chat window + await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click()); + await this.hideHomeMenu(); + // unfold chat window + await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click()); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "XDU for the win !", + "Chat window composer should still have the same input after hiding home menu" + ); + assert.containsN( + document.body, + '.o_Composer .o_Attachment', + 2, + "Chat window composer should have 2 attachments after hiding home menu" + ); + + // fold chat window + await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click()); + await this.showHomeMenu(); + // unfold chat window + await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click()); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInput_textarea`).value, + "XDU for the win !", + "chat window composer should still have the same input after showing home menu" + ); + assert.containsN( + document.body, + '.o_Composer .o_Attachment', + 2, + "Chat window composer should have 2 attachments after showing home menu" + ); +}); + +QUnit.test('[technical] chat window with a thread: keep scroll position in message list on toggle home menu when folded', async function (assert) { + // technical as show/hide home menu simulation are involved and home menu implementation + // have side-effects on DOM that may make chat window components not work + assert.expect(2); + + // channel that is expected to be found in the messaging menu + // with random unique id, needed to link messages + this.data['mail.channel'].records.push({ id: 20 }); + for (let i = 0; i < 10; i++) { + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [20], + }); + } + await this.start(); + await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click()); + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => document.querySelector('.o_NotificationList_preview').click(), + message: "should wait until channel 20 scrolled to its last message after opening it from the messaging menu", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector('.o_ThreadView_messageList'); + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, + }); + // Set a scroll position to chat window + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142, + message: "should wait until channel 20 scrolled to 142 after setting this value manually", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === 142 + ); + }, + }); + // fold chat window + await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click()); + await this.hideHomeMenu(); + // unfold chat window + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => document.querySelector('.o_ChatWindow_header').click(), + message: "should wait until channel 20 restored its scroll to 142 after unfolding it", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same after home menu is hidden" + ); + + // fold chat window + await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click()); + // Show home menu + await this.showHomeMenu(); + // unfold chat window + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => document.querySelector('.o_ChatWindow_header').click(), + message: "should wait until channel 20 restored its scroll position to the last saved value (142)", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === 'mail.channel' && + thread.id === 20 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same after home menu is shown" + ); +}); + +QUnit.test('chat window does not fetch messages if hidden', async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: 900px) + * (others: @see `mail/static/src/models/chat_window_manager/chat_window_manager.js:visual`) + * + * - chat window width: 325px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 200px + * - global width: 1080px + * + * Enough space for 2 visible chat windows, and one hidden chat window: + * 3 visible chat windows: + * 10 + 325 + 5 + 325 + 5 + 325 + 10 = 1000 > 900 + * 2 visible chat windows + hidden menu: + * 10 + 325 + 5 + 325 + 10 + 200 + 5 = 875 < 900 + */ + assert.expect(14); + + // 3 channels are expected to be found in the messaging menu, each with a + // random unique id that will be referenced in the test + this.data['mail.channel'].records = [ + { + id: 10, + is_minimized: true, + name: "Channel #10", + state: 'open', + }, + { + id: 11, + is_minimized: true, + name: "Channel #11", + state: 'open', + }, + { + id: 12, + is_minimized: true, + name: "Channel #12", + state: 'open', + }, + ]; + await this.start({ + env: { + browser: { + innerWidth: 900, + }, + }, + mockRPC(route, args) { + if (args.method === 'message_fetch') { + // domain should be like [['channel_id', 'in', [X]]] with X the channel id + const channel_ids = args.kwargs.domain[0][2]; + assert.strictEqual(channel_ids.length, 1, "messages should be fetched channel per channel"); + assert.step(`rpc:message_fetch:${channel_ids[0]}`); + } + return this._super(...arguments); + }, + }); + + assert.containsN( + document.body, + '.o_ChatWindow', + 2, + "2 chat windows should be visible" + ); + assert.containsNone( + document.body, + `.o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 12, + model: 'mail.channel', + }).localId + }"]`, + "chat window for Channel #12 should be hidden" + ); + assert.containsOnce( + document.body, + '.o_ChatWindowHiddenMenu', + "chat window hidden menu should be displayed" + ); + assert.verifySteps( + ['rpc:message_fetch:10', 'rpc:message_fetch:11'], + "messages should be fetched for the two visible chat windows" + ); + + await afterNextRender(() => + document.querySelector('.o_ChatWindowHiddenMenu_dropdownToggle').click() + ); + assert.containsOnce( + document.body, + '.o_ChatWindowHiddenMenu_chatWindowHeader', + "1 hidden chat window should be listed in hidden menu" + ); + + await afterNextRender(() => + document.querySelector('.o_ChatWindowHiddenMenu_chatWindowHeader').click() + ); + assert.containsN( + document.body, + '.o_ChatWindow', + 2, + "2 chat windows should still be visible" + ); + assert.containsOnce( + document.body, + `.o_ChatWindow[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 12, + model: 'mail.channel', + }).localId + }"]`, + "chat window for Channel #12 should now be visible" + ); + assert.verifySteps( + ['rpc:message_fetch:12'], + "messages should now be fetched for Channel #12" + ); +}); + +QUnit.test('new message separator is shown in a chat window of a chat on receiving new message if there is a history of conversation', async function (assert) { + assert.expect(3); + + this.data['res.partner'].records.push({ id: 10, name: "Demo" }); + this.data['res.users'].records.push({ + id: 42, + name: "Foreigner user", + partner_id: 10, + }); + this.data['mail.channel'].records = [ + { + channel_type: "chat", + id: 10, + is_minimized: true, + is_pinned: false, + members: [this.data.currentPartnerId, 10], + uuid: 'channel-10-uuid', + }, + ]; + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [10], + model: 'mail.channel', + res_id: 10, + }); + await this.start(); + + // simulate receiving a message + await afterNextRender(async () => this.env.services.rpc({ + route: '/mail/chat_post', + params: { + context: { + mockedUserId: 42, + }, + message_content: "hu", + uuid: 'channel-10-uuid', + }, + })); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "a chat window should be visible after receiving a new message from a chat" + ); + assert.containsN( + document.body, + '.o_Message', + 2, + "chat window should have 2 messages" + ); + assert.containsOnce( + document.body, + '.o_MessageList_separatorNewMessages', + "should display 'new messages' separator in the conversation, from reception of new messages" + ); +}); + +QUnit.test('new message separator is not shown in a chat window of a chat on receiving new message if there is no history of conversation', async function (assert) { + assert.expect(1); + + this.data['res.partner'].records.push({ id: 10, name: "Demo" }); + this.data['res.users'].records.push({ + id: 42, + name: "Foreigner user", + partner_id: 10, + }); + this.data['mail.channel'].records = [{ + channel_type: "chat", + id: 10, + members: [this.data.currentPartnerId, 10], + uuid: 'channel-10-uuid', + }]; + await this.start(); + + // simulate receiving a message + await afterNextRender(async () => this.env.services.rpc({ + route: '/mail/chat_post', + params: { + context: { + mockedUserId: 42, + }, + message_content: "hu", + uuid: 'channel-10-uuid', + }, + })); + assert.containsNone( + document.body, + '.o_MessageList_separatorNewMessages', + "should not display 'new messages' separator in the conversation of a chat on receiving new message if there is no history of conversation" + ); +}); + +QUnit.test('focusing a chat window of a chat should make new message separator disappear [REQUIRE FOCUS]', async function (assert) { + assert.expect(2); + + this.data['res.partner'].records.push({ id: 10, name: "Demo" }); + this.data['res.users'].records.push({ + id: 42, + name: "Foreigner user", + partner_id: 10, + }); + this.data['mail.channel'].records.push( + { + channel_type: "chat", + id: 10, + is_minimized: true, + is_pinned: false, + members: [this.data.currentPartnerId, 10], + message_unread_counter: 0, + uuid: 'channel-10-uuid', + }, + ); + this.data['mail.message'].records.push({ + body: "not empty", + channel_ids: [10], + model: 'mail.channel', + res_id: 10, + }); + await this.start(); + + // simulate receiving a message + await afterNextRender(() => this.env.services.rpc({ + route: '/mail/chat_post', + params: { + context: { + mockedUserId: 42, + }, + message_content: "hu", + uuid: 'channel-10-uuid', + }, + })); + assert.containsOnce( + document.body, + '.o_MessageList_separatorNewMessages', + "should display 'new messages' separator in the conversation, from reception of new messages" + ); + + await afterNextRender(() => this.afterEvent({ + eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', + func: () => document.querySelector('.o_ComposerTextInput_textarea').focus(), + message: "should wait until last seen by current partner message id changed", + predicate: ({ thread }) => { + return ( + thread.id === 10 && + thread.model === 'mail.channel' + ); + }, + })); + assert.containsNone( + document.body, + '.o_MessageList_separatorNewMessages', + "new message separator should no longer be shown, after focus on composer text input of chat window" + ); +}); + +QUnit.test('chat window should remain folded when new message is received', async function (assert) { + assert.expect(1); + + this.data['res.partner'].records.push({ id: 10, name: "Demo" }); + this.data['res.users'].records.push({ + id: 42, + name: "Foreigner user", + partner_id: 10, + }); + this.data['mail.channel'].records = [ + { + channel_type: "chat", + id: 10, + is_minimized: true, + is_pinned: false, + members: [this.data.currentPartnerId, 10], + state: 'folded', + uuid: 'channel-10-uuid', + }, + ]; + + await this.start(); + // simulate receiving a new message + await afterNextRender(async () => this.env.services.rpc({ + route: '/mail/chat_post', + params: { + context: { + mockedUserId: 42, + }, + message_content: "New Message 2", + uuid: 'channel-10-uuid', + }, + })); + assert.hasClass( + document.querySelector(`.o_ChatWindow`), + 'o-folded', + "chat window should remain folded" + ); +}); + +}); +}); +}); + +}); |
