diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js index 3b405761169..2142a21da4a 100644 --- a/addons/mail/static/src/js/discuss.js +++ b/addons/mail/static/src/js/discuss.js @@ -99,7 +99,7 @@ var PartnerInviteDialog = Dialog.extend({ self.do_notify(_t('New people'), notification); // Clear the membersDeferred to fetch again the partner // when getMentionPartnerSuggestions from the chatManager is triggered - var channel = this.call('chat_manager', 'getChannel', self.channelID); + var channel = self.call('chat_manager', 'getChannel', self.channelID); delete channel.membersDeferred; }); } @@ -508,7 +508,7 @@ var Discuss = Widget.extend(ControlPanelMixin, { */ _selectMessage: function (messageID) { this.$el.addClass('o_mail_selection_mode'); - var message = this._getMessage(messageID); + var message = this.call('chat_manager', 'getMessage', messageID);; this.selected_message = message; var subject = "Re: " + message.record_name; this.extendedComposer.set_subject(subject); @@ -903,10 +903,10 @@ var Discuss = Widget.extend(ControlPanelMixin, { if (def) { def.then(function (value) { if (value !== 'granted') { - self._sendNotification(self, _t('Permission denied'), + self.call('bus_service', 'sendNotification', self, _t('Permission denied'), _t('Odoo will not have the permission to send native notifications on this device.')); } else { - self._sendNotification(self, _t('Permission granted'), + self.call('bus_service', 'sendNotification', self, _t('Permission granted'), _t('Odoo has now the permission to send you native notifications on this device.')); } }); @@ -963,7 +963,7 @@ var Discuss = Widget.extend(ControlPanelMixin, { * @private */ _onUnsubscribeButtonClicked: function () { - this._unsubscribe(this.channel); + this.call('chat_manager', 'unsubscribe', this.channel); }, }); diff --git a/addons/mail/static/src/js/services/chat_manager.js b/addons/mail/static/src/js/services/chat_manager.js index fb4b6672bc6..a58de02365a 100644 --- a/addons/mail/static/src/js/services/chat_manager.js +++ b/addons/mail/static/src/js/services/chat_manager.js @@ -352,7 +352,6 @@ var ChatManager = AbstractService.extend({ * => list containing a list of members (cached by chat_manager) */ getMentionPartnerSuggestions: function (channel) { - var self = this; if (!channel) { return this.mentionPartnerSuggestions; } @@ -365,14 +364,7 @@ var ChatManager = AbstractService.extend({ shadow: true }) .then(function (members) { - var suggestions = []; - _.each(self.mentionPartnerSuggestions, function (partners) { - suggestions.push(_.filter(partners, function (partner) { - return !_.findWhere(members, { id: partner.id }); - })); - }); - - return [suggestions]; + return [members]; }); } return channel.membersDeferred; @@ -1642,7 +1634,7 @@ var ChatManager = AbstractService.extend({ web_client.set_title_part("_chat", tabTitle); } - this._sendNotification(web_client, title, content); + this.call('bus_service', 'sendNotification', web_client, title, content); }, /** * Removes channel diff --git a/addons/mail/static/src/js/services/chat_window_manager.js b/addons/mail/static/src/js/services/chat_window_manager.js index bab6fbf7729..bea6eaf0ace 100644 --- a/addons/mail/static/src/js/services/chat_window_manager.js +++ b/addons/mail/static/src/js/services/chat_window_manager.js @@ -387,8 +387,8 @@ var ChatWindowManager = AbstractService.extend({ this.displayState.hiddenSessions = hiddenSessions; this.displayState.hiddenUnreadCounter = hiddenUnreadCounter; - if (this.displayState.$hidden_windows_dropdown) { - this.displayState.$hidden_windows_dropdown.remove(); + if (this.displayState.$hiddenWindowsDropdown) { + this.displayState.$hiddenWindowsDropdown.remove(); } if (hiddenSessions.length) { this.displayState.$hiddenWindowsDropdown = this._renderHiddenSessionsDropdown(); @@ -487,8 +487,8 @@ var ChatWindowManager = AbstractService.extend({ self.displayState.hiddenUnreadCounter += session.window.unread_msgs; } }); - if (self.displayState.$hidden_windows_dropdown) { - self.displayState.$hidden_windows_dropdown.html(self.renderHiddenSessionsDropdown().html()); + if (self.displayState.$hiddenWindowsDropdown) { + self.displayState.$hiddenWindowsDropdown.html(self._renderHiddenSessionsDropdown().html()); self._repositionHiddenSessionsDropdown(); } }, diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 4ef63b70615..e247c01430a 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -369,6 +369,8 @@ QUnit.test('kanban activity widget with an activity', function (assert) { QUnit.test('chatter: post, receive and star messages', function (assert) { var done = assert.async(); assert.expect(28); + + testUtils.patchWindowGetSelection(); var bus = new Bus(); var BusService = createBusService(bus); @@ -533,7 +535,11 @@ QUnit.test('chatter: post, receive and star messages', function (assert) { assert.strictEqual($(".o_composer_input a").length, 1, "mention is 'green' in edit mode"); BasicComposer.prototype.MENTION_THROTTLE = mentionThrottle; + + //cleanup form.destroy(); + testUtils.unpatchWindowGetSelection(); + done(); }); }); diff --git a/addons/mail/static/tests/discuss_mobile_tests.js b/addons/mail/static/tests/discuss_mobile_tests.js index 4e96c87f512..d17364b53ad 100644 --- a/addons/mail/static/tests/discuss_mobile_tests.js +++ b/addons/mail/static/tests/discuss_mobile_tests.js @@ -29,21 +29,13 @@ QUnit.test('mobile basic rendering', function (assert) { assert.expect(9); var done = assert.async(); - var discuss = createDiscuss({ + createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, - mockRPC: function (route, args) { - if (args.method === 'message_fetch') { - return $.when([]); - } - return this._super.apply(this, arguments); - }, - }); - - discuss.call('chat_manager', 'isReady').then(function () { + }).then(function (discuss) { // test basic rendering in mobile assert.strictEqual(discuss.$('.o_mail_chat_mobile_control_panel').length, 1, "should have rendered a control panel"); diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js index 7f48e3b0450..348a6357b5d 100644 --- a/addons/mail/static/tests/discuss_tests.js +++ b/addons/mail/static/tests/discuss_tests.js @@ -1,12 +1,18 @@ odoo.define('mail.discuss_test', function (require) { "use strict"; -var testUtils = require('web.test_utils'); -var Widget = require('web.Widget'); - +var ChatManager = require('mail.ChatManager'); +var Composers = require('mail.composer'); var mailTestUtils = require('mail.testUtils'); +var concurrency = require('web.concurrency'); +var testUtils = require('web.test_utils'); + +var BasicComposer = Composers.BasicComposer; +var createBusService = mailTestUtils.createBusService; var createDiscuss = mailTestUtils.createDiscuss; +var patchWindowGetSelection = testUtils.patchWindowGetSelection; +var unpatchWindowGetSelection = testUtils.unpatchWindowGetSelection; QUnit.module('mail', {}, function () { @@ -17,57 +23,125 @@ QUnit.module('Discuss client action', { fields: {}, }, }; + this.services = [ChatManager, createBusService()]; }, }); -QUnit.skip('mobile basic rendering', function (assert) { - // Unfortunately, this test is skipped for now because there is no way to - // execute the whole test suite in mobile (it only works test by test), so - // as the client action include for mobile is rejected when we are not in - // mobile, it isn't possible to test it - // Moreover, RPCs done by the chat_manager (e.g. message_fetch) should be - // properly mocked. - assert.expect(11); +QUnit.test('basic rendering', function (assert) { + assert.expect(5); + var done = assert.async(); - var parent = new Widget(); - testUtils.addMockEnvironment(parent, { - data: this.data, - archs: { - 'mail.message,false,search': '', - }, - config: {device: {isMobile: true}}, - }); - - var params = { + createDiscuss({ id: 1, context: {}, params: {}, + data: this.data, + services: this.services, + }) + .then(function (discuss) { + // test basic rendering + var $sidebar = discuss.$('.o_mail_chat_sidebar'); + assert.strictEqual($sidebar.length, 1, + "should have rendered a sidebar"); + + var $nocontent = discuss.$('.o_mail_chat_content'); + assert.strictEqual($nocontent.length, 1, + "should have rendered the content"); + assert.strictEqual($nocontent.find('.o_mail_no_content').length, 1, + "should display no content message"); + + var $inbox = $sidebar.find('.o_mail_chat_channel_item[data-channel-id=channel_inbox]'); + assert.strictEqual($inbox.length, 1, + "should have the channel item 'channel_inbox' in the sidebar"); + + var $starred = $sidebar.find('.o_mail_chat_channel_item[data-channel-id=channel_starred]'); + assert.strictEqual($starred.length, 1, + "should have the channel item 'channel_starred' in the sidebar"); + discuss.destroy(); + done(); + }); +}); + +QUnit.test('@ mention in channel', function (assert) { + assert.expect(9); + var done = assert.async(); + + this.data.initMessaging = { + channel_slots: { + channel_channel: [{ + id: 1, + channel_type: "channel", + name: "general", + }], + }, }; - var discuss = createDiscuss(params); - // test basic rendering in mobile - assert.equal(discuss.$(".o_mail_chat_mobile_control_panel").length, 1, "Mobile control panel created"); - assert.equal(discuss.$(".o_mail_mobile_tab").length, 4, "Four mobile tabs created"); - assert.equal(discuss.$('.o_mail_chat_content').length, 1, "One default chat content pane created"); - assert.equal(discuss.$(".o_mail_chat_tab_pane").length, 3, "Three mobile tab panes created"); + // Remove the mention throttle to speed up the test + var mentionThrottle = BasicComposer.prototype.MENTION_THROTTLE; + BasicComposer.prototype.MENTION_THROTTLE = 1; - // Inbox - assert.equal(discuss.activeMobileTab, "channel_inbox", "'channel_inbox' is default active tab"); - assert.ok(discuss.$(".o_channel_inbox_item:nth(0)").hasClass("btn-primary"), "Showing 'Inbox'"); + var fetchListenersDef = $.Deferred(); - // Starred - discuss.$(".o_channel_inbox_item[data-type='channel_starred']").click(); - assert.ok(discuss.$(".o_channel_inbox_item:nth(1)").hasClass("btn-primary"), "Clicked on 'Starred'"); + createDiscuss({ + id: 1, + context: {}, + params: {}, + data: this.data, + services: this.services, + mockRPC: function (route, args) { + if (args.method === 'channel_fetch_listeners') { + fetchListenersDef.resolve(); + return $.when([ { id: 2, name: 'TestUser'} ]); + } + return this._super.apply(this, arguments); + }, + }) + .then(function (discuss) { + var $general = discuss.$('.o_mail_chat_sidebar') + .find('.o_mail_chat_channel_item[data-channel-id=1]'); + assert.strictEqual($general.length, 1, + "should have the channel item with id 1"); + assert.strictEqual($general.attr('title'), 'general', + "should have the title 'general'"); - assert.ok(discuss.$(".o_mail_chat_content").is(":visible"), "Default main content pane visible"); + // click on general + $general.click(); + var $input = discuss.$('.o_composer_input').first(); + assert.ok($input.length, "should display a composer input"); - discuss.$(".o_mail_mobile_tab[data-type='dm']").click(); - assert.equal(discuss.activeMobileTab, "dm", "After click on 'Conversation', is now active tab"); + // Simulate '@' typed by user with mocked Window.getSelection + // Note: focus is needed in order to trigger rpc 'channel_fetch_listeners' + $input.focus(); + $input.text("@"); + patchWindowGetSelection(); + $input.trigger('keyup'); - assert.ok(!discuss.$(".o_mail_chat_content").is(":visible"), "none", "'Main' content pane is invisible"); - assert.ok(discuss.$(".o_mail_chat_tab_pane:nth(0)").is(":visible"), "'Conversation' pane is visible"); + fetchListenersDef + .then(concurrency.delay.bind(concurrency, 0)) + .then(function () { + assert.strictEqual(discuss.$('.dropup.o_composer_mention_dropdown.open').length, 1, + "dropup menu for partner mentions should be open"); - discuss.destroy(); + var $mentionProposition = discuss.$('.o_mention_proposition'); + assert.strictEqual($mentionProposition.length, 1, + "should display one partner mention proposition"); + assert.strictEqual($mentionProposition.data('id'), 2, + "partner mention should link to the correct partner id"); + assert.strictEqual($mentionProposition.find('.o_mention_name').text(), "TestUser", + "partner mention should display the correct partner name"); + + $mentionProposition.click(); + assert.strictEqual(discuss.$('.o_mention_proposition').length, 0, + "should not have any partner mention proposition after clicking on it"); + assert.strictEqual($input.find('a').text() , "@TestUser", + "should have the correct mention link in the composer input"); + + BasicComposer.prototype.MENTION_THROTTLE = mentionThrottle; + unpatchWindowGetSelection(); + discuss.destroy(); + done(); + }); + }); }); }); diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js index 365b067b55a..a2aef300736 100644 --- a/addons/mail/static/tests/helpers/mock_server.js +++ b/addons/mail/static/tests/helpers/mock_server.js @@ -5,14 +5,23 @@ var MockServer = require('web.MockServer'); MockServer.include({ /** + * Param 'data' may have a key 'initMessaging' which contains + * a partial overwrite of the result from mockInitMessaging. + * + * Note: we must delete this key, so that this is not + * handled as a model definition. + * * @override + * @param {Object} [data.initMessaging] */ - _performRpc: function (route, args) { - if (route === '/mail/init_messaging') { - return $.when(this._mockInitMessaging(args)); + init: function (data, options) { + if (data && data.initMessaging) { + this.initMessagingData = data.initMessaging; + delete data.initMessaging; } - return this._super(route, args); + this._super.apply(this, arguments); }, + /** * Simulate the '/mail/init_messaging' route * @@ -20,7 +29,7 @@ MockServer.include({ * @return {Object} */ _mockInitMessaging: function () { - return { + return _.defaults(this.initMessagingData || {}, { 'needaction_inbox_counter': 0, 'starred_counter': 0, 'channel_slots': [], @@ -28,7 +37,19 @@ MockServer.include({ 'mention_partner_suggestions': [], 'shortcodes': [], 'menu_id': false, - }; + }); + }, + /** + * @override + */ + _performRpc: function (route, args) { + if (route === '/mail/init_messaging') { + return $.when(this._mockInitMessaging(args)); + } + if (args.method === 'message_fetch') { + return $.when([]); + } + return this._super(route, args); }, }); diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js index e0a8a0a13de..095da27be58 100644 --- a/addons/mail/static/tests/helpers/test_utils.js +++ b/addons/mail/static/tests/helpers/test_utils.js @@ -55,24 +55,31 @@ function createBusService(bus) { return BusService; } +/** + * Create asynchronously a discuss widget. + * This is async due to chat_manager service that needs to be ready. + * + * @param {Object} params + * @return {$.Promise} resolved with the discuss widget + */ function createDiscuss(params) { var Parent = Widget.extend({ do_push_state: function () {}, }); var parent = new Parent(); - testUtils.addMockEnvironment(parent, { - data: params.data, + testUtils.addMockEnvironment(parent, _.extend(params, { archs: { 'mail.message,false,search': '', }, - mockRPC: params.mockRPC, - services: params.services, - }); + })); var discuss = new Discuss(parent, params); discuss.set_cp_bus(new Widget()); - discuss.appendTo($('#qunit-fixture')); + var selector = params.debug ? 'body' : '#qunit-fixture'; + discuss.appendTo($(selector)); - return discuss; + return discuss.call('chat_manager', 'isReady').then(function () { + return discuss; + }); } return { diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index 28d92c9e157..6769462d404 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -54,6 +54,8 @@ diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index a4f6531d075..6f3b3ef4549 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -23,6 +23,8 @@ var Widget = require('web.Widget'); var DebouncedField = basic_fields.DebouncedField; +var originalWindowGetSelection; // cached replaced originalGetSelection + /** * intercepts an event bubbling up the widget hierarchy. The event intercepted * must be a "custom event", i.e. an event generated by the method 'trigger_up'. @@ -725,6 +727,50 @@ function patch (target, props) { }); } } +/** + * In Phantomjs, there is a crash when calling window.getSelection + * in order for the tests to work, for the specific test that uses it, replace + * the default window.getSelection by a mock + * + * usage: + * QUnit.test('...',function(done){ + * testUtils.patchWindowGetSelection(); + * + * // do something that needs to use window.getSelection() + * assert.strictEqual(....); + * + * // restore the original function + * testUtils.unpatchWindowGetSelection(); + * + * // finish the test + * done(); + * }) + */ + +function patchWindowGetSelection() { + originalWindowGetSelection = window.getSelection; + window.getSelection = function () { + return { + removeAllRanges: function () {}, + addRange: function (range) {}, + getRangeAt: function (index) { + return { + startOffset : 0 + }; + }, + anchorNode: { + parentNode: { + childNodes: [{ + outerHTML: "@", + nodeType: 3, + textContent: '@', + }], + }, + }, + }; + }; +} + /** * Unpatches a given Class or Object. * @@ -749,6 +795,14 @@ function unpatch(target) { delete target.__patchID; } +/** + * after using patchWindowGetSelection, at the end of your test + * you must restore the default function so there is no impact to the other tests + */ +function unpatchWindowGetSelection() { + window.getSelection = originalWindowGetSelection; +} + // Loading static files cannot be properly simulated when their real content is // really needed. This is the case for static XML files so we load them here, // before starting the qunit test suite. @@ -775,11 +829,13 @@ return $.when( intercept: intercept, observe: observe, patch: patch, + patchWindowGetSelection: patchWindowGetSelection, removeSrcAttribute: removeSrcAttribute, triggerKeypressEvent: triggerKeypressEvent, triggerMouseEvent: triggerMouseEvent, triggerPositionalMouseEvent: triggerPositionalMouseEvent, unpatch: unpatch, + unpatchWindowGetSelection: unpatchWindowGetSelection, }; });