[MERGE] mail,web: fix various bugs from recent JS mail refactoring

Revision on a recent JS refactoring: https://github.com/odoo/odoo/commit/02ec09cb1c3e2d7bc7968f40c18f2208d7f3f498

This merge contains several fixes from the above refactoring commit.

It also adds tests on discuss, as one of the purpose of the refactoring
was to test this widget.
This commit is contained in:
Alexandre Kühn
2018-02-20 14:17:57 +01:00
10 changed files with 231 additions and 81 deletions
+5 -5
View File
@@ -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);
},
});
@@ -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
@@ -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();
}
},
@@ -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();
});
});
@@ -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");
+113 -39
View File
@@ -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': '<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();
});
});
});
});
@@ -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);
},
});
+14 -7
View File
@@ -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': '<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 {
+2
View File
@@ -54,6 +54,8 @@
<template id="qunit_mobile_suite" name="mail_mobile_tests" inherit_id="web.qunit_mobile_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/mail/static/tests/discuss_mobile_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/helpers/mock_server.js"></script>
<script type="text/javascript" src="/mail/static/tests/helpers/test_utils.js"></script>
</xpath>
</template>
@@ -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,
};
});